open-multi-agent-kit 0.80.4 → 0.80.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/config.d.ts +4 -9
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +46 -15
  5. package/dist/config.js.map +1 -1
  6. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  7. package/dist/core/compaction/branch-summarization.js +6 -1
  8. package/dist/core/compaction/branch-summarization.js.map +1 -1
  9. package/dist/core/compaction/compaction.d.ts +82 -1
  10. package/dist/core/compaction/compaction.d.ts.map +1 -1
  11. package/dist/core/compaction/compaction.js +606 -10
  12. package/dist/core/compaction/compaction.js.map +1 -1
  13. package/dist/core/harness-control-events.d.ts +79 -0
  14. package/dist/core/harness-control-events.d.ts.map +1 -0
  15. package/dist/core/harness-control-events.js +339 -0
  16. package/dist/core/harness-control-events.js.map +1 -0
  17. package/dist/core/harness-control-replay.d.ts +26 -0
  18. package/dist/core/harness-control-replay.d.ts.map +1 -0
  19. package/dist/core/harness-control-replay.js +155 -0
  20. package/dist/core/harness-control-replay.js.map +1 -0
  21. package/dist/core/keybindings.d.ts +12 -0
  22. package/dist/core/keybindings.d.ts.map +1 -1
  23. package/dist/core/keybindings.js +31 -1
  24. package/dist/core/keybindings.js.map +1 -1
  25. package/dist/core/mcp-inventory.d.ts +40 -0
  26. package/dist/core/mcp-inventory.d.ts.map +1 -0
  27. package/dist/core/mcp-inventory.js +104 -0
  28. package/dist/core/mcp-inventory.js.map +1 -0
  29. package/dist/core/settings-manager.d.ts +3 -0
  30. package/dist/core/settings-manager.d.ts.map +1 -1
  31. package/dist/core/settings-manager.js +4 -0
  32. package/dist/core/settings-manager.js.map +1 -1
  33. package/dist/core/slash-commands.d.ts.map +1 -1
  34. package/dist/core/slash-commands.js +4 -1
  35. package/dist/core/slash-commands.js.map +1 -1
  36. package/dist/core/spec-kit/compiler.d.ts +67 -0
  37. package/dist/core/spec-kit/compiler.d.ts.map +1 -0
  38. package/dist/core/spec-kit/compiler.js +268 -0
  39. package/dist/core/spec-kit/compiler.js.map +1 -0
  40. package/dist/core/transaction-coordinator.d.ts +30 -0
  41. package/dist/core/transaction-coordinator.d.ts.map +1 -0
  42. package/dist/core/transaction-coordinator.js +173 -0
  43. package/dist/core/transaction-coordinator.js.map +1 -0
  44. package/dist/migrations.d.ts +35 -0
  45. package/dist/migrations.d.ts.map +1 -1
  46. package/dist/migrations.js +409 -3
  47. package/dist/migrations.js.map +1 -1
  48. package/dist/modes/interactive/components/control-panel.d.ts +29 -0
  49. package/dist/modes/interactive/components/control-panel.d.ts.map +1 -0
  50. package/dist/modes/interactive/components/control-panel.js +90 -0
  51. package/dist/modes/interactive/components/control-panel.js.map +1 -0
  52. package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
  53. package/dist/modes/interactive/components/custom-editor.js +21 -13
  54. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  55. package/dist/modes/interactive/components/index.d.ts +3 -0
  56. package/dist/modes/interactive/components/index.d.ts.map +1 -1
  57. package/dist/modes/interactive/components/index.js +3 -0
  58. package/dist/modes/interactive/components/index.js.map +1 -1
  59. package/dist/modes/interactive/components/mcp-selector.d.ts +28 -0
  60. package/dist/modes/interactive/components/mcp-selector.d.ts.map +1 -0
  61. package/dist/modes/interactive/components/mcp-selector.js +138 -0
  62. package/dist/modes/interactive/components/mcp-selector.js.map +1 -0
  63. package/dist/modes/interactive/components/model-selector.d.ts +12 -3
  64. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  65. package/dist/modes/interactive/components/model-selector.js +120 -27
  66. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  67. package/dist/modes/interactive/components/settings-selector.d.ts +19 -1
  68. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  69. package/dist/modes/interactive/components/settings-selector.js +21 -8
  70. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  71. package/dist/modes/interactive/components/skills-selector.d.ts +32 -0
  72. package/dist/modes/interactive/components/skills-selector.d.ts.map +1 -0
  73. package/dist/modes/interactive/components/skills-selector.js +130 -0
  74. package/dist/modes/interactive/components/skills-selector.js.map +1 -0
  75. package/dist/modes/interactive/components/thinking-selector.d.ts +7 -8
  76. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  77. package/dist/modes/interactive/components/thinking-selector.js +7 -48
  78. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  79. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  80. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  81. package/dist/modes/interactive/interactive-mode.js +340 -97
  82. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  83. package/dist/modes/interactive/theme/catppuccin-mocha.json +86 -0
  84. package/dist/modes/interactive/theme/dracula.json +86 -0
  85. package/dist/modes/interactive/theme/gruvbox-dark.json +86 -0
  86. package/dist/modes/interactive/theme/kanagawa-dragon.json +86 -0
  87. package/dist/modes/interactive/theme/omk-control-panel.json +91 -0
  88. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  89. package/dist/modes/interactive/theme/theme.js +41 -16
  90. package/dist/modes/interactive/theme/theme.js.map +1 -1
  91. package/dist/modes/interactive/theme/tokyo-night-storm.json +86 -0
  92. package/docs/compaction.md +21 -8
  93. package/docs/quickstart.md +1 -1
  94. package/docs/themes.md +10 -2
  95. package/docs/usage.md +2 -1
  96. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  97. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  98. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  99. package/examples/extensions/gondolin/package-lock.json +2 -2
  100. package/examples/extensions/gondolin/package.json +1 -1
  101. package/examples/extensions/sandbox/package-lock.json +2 -2
  102. package/examples/extensions/sandbox/package.json +1 -1
  103. package/examples/extensions/with-deps/package-lock.json +2 -2
  104. package/examples/extensions/with-deps/package.json +1 -1
  105. package/package.json +4 -4
@@ -6,7 +6,7 @@ import * as crypto from "node:crypto";
6
6
  import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
- import { getProviders, } from "@earendil-works/omk-ai";
9
+ import { clampThinkingLevel, getProviders, getSupportedThinkingLevels, modelsAreEqual, } from "@earendil-works/omk-ai";
10
10
  import { CombinedAutocompleteProvider, Container, fuzzyFilter, getCapabilities, hyperlink, Loader, Markdown, matchesKey, ProcessTerminal, Spacer, setKeybindings, Text, TruncatedText, TUI, visibleWidth, } from "@earendil-works/omk-tui";
11
11
  import chalk from "chalk";
12
12
  import { spawn, spawnSync } from "child_process";
@@ -16,6 +16,7 @@ import { SessionImportFileNotFoundError } from "../../core/agent-session-runtime
16
16
  import { FooterDataProvider } from "../../core/footer-data-provider.js";
17
17
  import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.js";
18
18
  import { KeybindingsManager } from "../../core/keybindings.js";
19
+ import { loadMcpInventory } from "../../core/mcp-inventory.js";
19
20
  import { createCompactionSummaryMessage } from "../../core/messages.js";
20
21
  import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.js";
21
22
  import { DefaultPackageManager } from "../../core/package-manager.js";
@@ -24,6 +25,7 @@ import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../cor
24
25
  import { SessionManager } from "../../core/session-manager.js";
25
26
  import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.js";
26
27
  import { isInstallTelemetryEnabled } from "../../core/telemetry.js";
28
+ import { runHarnessControlTransaction, runHarnessControlTransactionSync } from "../../core/transaction-coordinator.js";
27
29
  import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.js";
28
30
  import { copyToClipboard } from "../../utils/clipboard.js";
29
31
  import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.js";
@@ -38,6 +40,7 @@ import { BashExecutionComponent } from "./components/bash-execution.js";
38
40
  import { BorderedLoader } from "./components/bordered-loader.js";
39
41
  import { BranchSummaryMessageComponent } from "./components/branch-summary-message.js";
40
42
  import { CompactionSummaryMessageComponent } from "./components/compaction-summary-message.js";
43
+ import { ControlPanelComponent } from "./components/control-panel.js";
41
44
  import { CountdownTimer } from "./components/countdown-timer.js";
42
45
  import { CustomEditor } from "./components/custom-editor.js";
43
46
  import { CustomMessageComponent } from "./components/custom-message.js";
@@ -50,12 +53,14 @@ import { ExtensionSelectorComponent } from "./components/extension-selector.js";
50
53
  import { FooterComponent } from "./components/footer.js";
51
54
  import { formatKeyText, keyDisplayText, keyHint, keyText, rawKeyHint } from "./components/keybinding-hints.js";
52
55
  import { LoginDialogComponent } from "./components/login-dialog.js";
56
+ import { McpSelectorComponent } from "./components/mcp-selector.js";
53
57
  import { ModelSelectorComponent } from "./components/model-selector.js";
54
58
  import { OAuthSelectorComponent } from "./components/oauth-selector.js";
55
59
  import { ScopedModelsSelectorComponent } from "./components/scoped-models-selector.js";
56
60
  import { SessionSelectorComponent } from "./components/session-selector.js";
57
- import { SettingsSelectorComponent } from "./components/settings-selector.js";
61
+ import { SettingsSelectorComponent, ThinkingSelectorComponent } from "./components/settings-selector.js";
58
62
  import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.js";
63
+ import { SkillsSelectorComponent } from "./components/skills-selector.js";
59
64
  import { ToolExecutionComponent } from "./components/tool-execution.js";
60
65
  import { TreeSelectorComponent } from "./components/tree-selector.js";
61
66
  import { UserMessageComponent } from "./components/user-message.js";
@@ -67,14 +72,21 @@ function isExpandable(obj) {
67
72
  class ExpandableText extends Text {
68
73
  getCollapsedText;
69
74
  getExpandedText;
75
+ expanded;
70
76
  constructor(getCollapsedText, getExpandedText, expanded = false, paddingX = 0, paddingY = 0) {
71
77
  super(expanded ? getExpandedText() : getCollapsedText(), paddingX, paddingY);
72
78
  this.getCollapsedText = getCollapsedText;
73
79
  this.getExpandedText = getExpandedText;
80
+ this.expanded = expanded;
74
81
  }
75
82
  setExpanded(expanded) {
83
+ this.expanded = expanded;
76
84
  this.setText(expanded ? this.getExpandedText() : this.getCollapsedText());
77
85
  }
86
+ invalidate() {
87
+ this.setText(this.expanded ? this.getExpandedText() : this.getCollapsedText());
88
+ super.invalidate();
89
+ }
78
90
  }
79
91
  const DEAD_TERMINAL_ERROR_CODES = new Set(["EIO", "EPIPE", "ENOTCONN"]);
80
92
  function isDeadTerminalError(error) {
@@ -315,6 +327,20 @@ export class InteractiveMode {
315
327
  name: command.name,
316
328
  description: command.description,
317
329
  }));
330
+ const thinkCommand = slashCommands.find((command) => command.name === "think");
331
+ if (thinkCommand) {
332
+ thinkCommand.getArgumentCompletions = (prefix) => {
333
+ const levels = this.session.getAvailableThinkingLevels();
334
+ const filtered = fuzzyFilter(levels, prefix, (level) => level);
335
+ if (filtered.length === 0)
336
+ return null;
337
+ return filtered.map((level) => ({
338
+ value: level,
339
+ label: level,
340
+ description: level === this.session.thinkingLevel ? "current" : undefined,
341
+ }));
342
+ };
343
+ }
318
344
  const modelCommand = slashCommands.find((command) => command.name === "model");
319
345
  if (modelCommand) {
320
346
  modelCommand.getArgumentCompletions = (prefix) => {
@@ -324,21 +350,48 @@ export class InteractiveMode {
324
350
  : this.session.modelRegistry.getAvailable();
325
351
  if (models.length === 0)
326
352
  return null;
327
- // Create items with provider/id format
328
- const items = models.map((m) => ({
329
- id: m.id,
330
- provider: m.provider,
331
- label: `${m.provider}/${m.id}`,
332
- }));
333
- // Fuzzy filter by model ID + provider (allows "opus anthropic" to match)
334
- const filtered = fuzzyFilter(items, prefix, (item) => `${item.id} ${item.provider}`);
335
- if (filtered.length === 0)
353
+ const normalizedPrefix = prefix.trimStart();
354
+ const slashIndex = normalizedPrefix.indexOf("/");
355
+ const providerPrefix = slashIndex >= 0 ? normalizedPrefix.slice(0, slashIndex) : normalizedPrefix;
356
+ const modelPrefix = slashIndex >= 0 ? normalizedPrefix.slice(slashIndex + 1) : "";
357
+ const providers = Array.from(new Set(models.map((model) => model.provider))).sort((a, b) => a.localeCompare(b));
358
+ if (slashIndex === -1) {
359
+ const providerItems = fuzzyFilter(providers.map((provider) => ({ provider })), providerPrefix, (item) => item.provider).map((item) => ({
360
+ value: `${item.provider}/`,
361
+ label: item.provider,
362
+ description: "provider",
363
+ }));
364
+ const modelItems = fuzzyFilter(models.map((model) => ({
365
+ id: model.id,
366
+ provider: model.provider,
367
+ label: `${model.provider}/${model.id}`,
368
+ })), normalizedPrefix, (item) => `${item.id} ${item.provider} ${item.provider}/${item.id}`).map((item) => ({
369
+ value: item.label,
370
+ label: item.id,
371
+ description: item.provider,
372
+ }));
373
+ const combined = [...providerItems, ...modelItems].slice(0, 50);
374
+ return combined.length > 0 ? combined : null;
375
+ }
376
+ const matchedProvider = providers.find((provider) => provider === providerPrefix);
377
+ if (!matchedProvider) {
336
378
  return null;
337
- return filtered.map((item) => ({
338
- value: item.label,
339
- label: item.id,
340
- description: item.provider,
379
+ }
380
+ const providerModels = models
381
+ .filter((model) => model.provider === matchedProvider)
382
+ .map((model) => ({
383
+ id: model.id,
384
+ provider: model.provider,
385
+ label: `${model.provider}/${model.id}`,
341
386
  }));
387
+ const filtered = fuzzyFilter(providerModels, modelPrefix, (item) => `${item.id} ${item.provider}`);
388
+ return filtered.length > 0
389
+ ? filtered.map((item) => ({
390
+ value: item.label,
391
+ label: item.id,
392
+ description: item.provider,
393
+ }))
394
+ : null;
342
395
  };
343
396
  }
344
397
  // Convert prompt templates to SlashCommand format for autocomplete
@@ -436,10 +489,9 @@ export class InteractiveMode {
436
489
  this.ui.addChild(this.headerContainer);
437
490
  // Add header with keybindings from config (unless silenced)
438
491
  if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
439
- const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
440
- // Build startup instructions using keybinding hint helpers
492
+ // Build startup instructions lazily so theme changes do not retain stale ANSI sequences.
441
493
  const hint = (keybinding, description) => keyHint(keybinding, description);
442
- const expandedInstructions = [
494
+ const expandedInstructions = () => [
443
495
  hint("app.interrupt", "to interrupt"),
444
496
  hint("app.clear", "to clear"),
445
497
  rawKeyHint(`${keyText("app.clear")} twice`, "to exit"),
@@ -460,16 +512,23 @@ export class InteractiveMode {
460
512
  hint("app.clipboard.pasteImage", "to paste image"),
461
513
  rawKeyHint("drop files", "to attach"),
462
514
  ].join("\n");
463
- const compactInstructions = [
515
+ const compactInstructions = () => [
464
516
  hint("app.interrupt", "interrupt"),
465
517
  rawKeyHint(`${keyText("app.clear")}/${keyText("app.exit")}`, "clear/exit"),
466
518
  rawKeyHint("/", "commands"),
467
519
  rawKeyHint("!", "bash"),
468
520
  hint("app.tools.expand", "more"),
469
521
  ].join(theme.fg("muted", " · "));
470
- const compactOnboarding = theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`);
471
- const onboarding = theme.fg("dim", `OMK can explain its own features and look up its docs. Ask it how to use or extend OMK.`);
472
- this.builtInHeader = new ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
522
+ const controlPanel = new ControlPanelComponent({
523
+ appName: APP_NAME,
524
+ version: this.version,
525
+ compactInstructions,
526
+ expandedInstructions,
527
+ compactOnboarding: () => `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`,
528
+ onboarding: () => `OMK can explain its own features and look up its docs. Ask it how to use or extend OMK.`,
529
+ });
530
+ controlPanel.setExpanded(this.getStartupExpansionState());
531
+ this.builtInHeader = controlPanel;
473
532
  // Setup UI layout
474
533
  this.headerContainer.addChild(new Spacer(1));
475
534
  this.headerContainer.addChild(this.builtInHeader);
@@ -980,22 +1039,53 @@ export class InteractiveMode {
980
1039
  }
981
1040
  return lines.join("\n");
982
1041
  }
1042
+ formatDiagnosticsSummary(diagnostics) {
1043
+ const collisionDiagnostics = diagnostics.filter((d) => d.type === "collision" && d.collision);
1044
+ const collisionNames = new Set(collisionDiagnostics.map((d) => `${d.collision?.resourceType ?? "resource"}:${d.collision?.name ?? "unknown"}`));
1045
+ const errorCount = diagnostics.filter((d) => d.type === "error").length;
1046
+ const warningCount = diagnostics.filter((d) => d.type === "warning").length;
1047
+ const parts = [];
1048
+ if (collisionDiagnostics.length > 0) {
1049
+ parts.push(`${collisionDiagnostics.length} duplicate resource${collisionDiagnostics.length === 1 ? "" : "s"} skipped across ${collisionNames.size} name${collisionNames.size === 1 ? "" : "s"}`);
1050
+ }
1051
+ if (errorCount > 0) {
1052
+ parts.push(`${errorCount} error${errorCount === 1 ? "" : "s"}`);
1053
+ }
1054
+ if (warningCount > 0) {
1055
+ parts.push(`${warningCount} warning${warningCount === 1 ? "" : "s"}`);
1056
+ }
1057
+ const summary = parts.length > 0 ? parts.join(", ") : "No issues";
1058
+ return theme.fg("dim", ` ${summary}. ${keyHint("app.tools.expand", "for details")}`);
1059
+ }
983
1060
  showLoadedResources(options) {
984
1061
  const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();
985
1062
  const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;
986
1063
  if (!showListing && !showDiagnostics) {
987
1064
  return;
988
1065
  }
1066
+ const diagnosticsForStartup = (diagnostics) => {
1067
+ if (showListing) {
1068
+ return [...diagnostics];
1069
+ }
1070
+ return diagnostics.filter((diagnostic) => diagnostic.type !== "collision");
1071
+ };
989
1072
  const sectionHeader = (name, color = "mdHeading") => theme.fg(color, `[${name}]`);
990
1073
  const formatCompactList = (items, options) => {
991
1074
  const labels = items.map((item) => item.trim()).filter((item) => item.length > 0);
992
1075
  if (options?.sort !== false) {
993
1076
  labels.sort((a, b) => a.localeCompare(b));
994
1077
  }
995
- return theme.fg("dim", ` ${labels.join(", ")}`);
1078
+ const maxItems = options?.maxItems ?? 24;
1079
+ const visibleLabels = labels.slice(0, maxItems);
1080
+ const overflow = labels.length - visibleLabels.length;
1081
+ const suffix = overflow > 0 ? `, … +${overflow} more (${keyText("app.tools.expand")} for full list)` : "";
1082
+ const body = visibleLabels.length > 0 ? `${visibleLabels.join(", ")}${suffix}` : "(none)";
1083
+ return theme.fg("dim", ` ${body}`);
996
1084
  };
997
1085
  const addLoadedSection = (name, collapsedBody, expandedBody = collapsedBody, color = "mdHeading") => {
998
- const section = new ExpandableText(() => `${sectionHeader(name, color)}\n${collapsedBody}`, () => `${sectionHeader(name, color)}\n${expandedBody}`, this.getStartupExpansionState(), 0, 0);
1086
+ const getCollapsedBody = typeof collapsedBody === "function" ? collapsedBody : () => collapsedBody;
1087
+ const getExpandedBody = typeof expandedBody === "function" ? expandedBody : () => expandedBody;
1088
+ const section = new ExpandableText(() => `${sectionHeader(name, color)}\n${getCollapsedBody()}`, () => `${sectionHeader(name, color)}\n${getExpandedBody()}`, this.getStartupExpansionState(), 0, 0);
999
1089
  this.chatContainer.addChild(section);
1000
1090
  this.chatContainer.addChild(new Spacer(1));
1001
1091
  };
@@ -1032,27 +1122,21 @@ export class InteractiveMode {
1032
1122
  const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
1033
1123
  if (contextFiles.length > 0) {
1034
1124
  this.chatContainer.addChild(new Spacer(1));
1035
- const contextList = contextFiles
1036
- .map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`))
1037
- .join("\n");
1038
- const contextCompactList = formatCompactList(contextFiles.map((contextFile) => this.formatContextPath(contextFile.path)), { sort: false });
1039
- addLoadedSection("Context", contextCompactList, contextList);
1125
+ addLoadedSection("Context", () => formatCompactList(contextFiles.map((contextFile) => this.formatContextPath(contextFile.path)), { sort: false }), () => contextFiles.map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`)).join("\n"));
1040
1126
  }
1041
1127
  const skills = skillsResult.skills;
1042
1128
  if (skills.length > 0) {
1043
1129
  const groups = this.buildScopeGroups(skills.map((skill) => ({ path: skill.filePath, sourceInfo: skill.sourceInfo })));
1044
- const skillList = this.formatScopeGroups(groups, {
1130
+ addLoadedSection("Skills", () => formatCompactList(skills.map((skill) => skill.name)), () => this.formatScopeGroups(groups, {
1045
1131
  formatPath: (item) => this.formatDisplayPath(item.path),
1046
1132
  formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
1047
- });
1048
- const skillCompactList = formatCompactList(skills.map((skill) => skill.name));
1049
- addLoadedSection("Skills", skillCompactList, skillList);
1133
+ }));
1050
1134
  }
1051
1135
  const templates = this.session.promptTemplates;
1052
1136
  if (templates.length > 0) {
1053
1137
  const groups = this.buildScopeGroups(templates.map((template) => ({ path: template.filePath, sourceInfo: template.sourceInfo })));
1054
1138
  const templateByPath = new Map(templates.map((t) => [t.filePath, t]));
1055
- const templateList = this.formatScopeGroups(groups, {
1139
+ addLoadedSection("Prompts", () => formatCompactList(templates.map((template) => `/${template.name}`)), () => this.formatScopeGroups(groups, {
1056
1140
  formatPath: (item) => {
1057
1141
  const template = templateByPath.get(item.path);
1058
1142
  return template ? `/${template.name}` : this.formatDisplayPath(item.path);
@@ -1061,18 +1145,14 @@ export class InteractiveMode {
1061
1145
  const template = templateByPath.get(item.path);
1062
1146
  return template ? `/${template.name}` : this.formatDisplayPath(item.path);
1063
1147
  },
1064
- });
1065
- const promptCompactList = formatCompactList(templates.map((template) => `/${template.name}`));
1066
- addLoadedSection("Prompts", promptCompactList, templateList);
1148
+ }));
1067
1149
  }
1068
1150
  if (extensions.length > 0) {
1069
1151
  const groups = this.buildScopeGroups(extensions);
1070
- const extList = this.formatScopeGroups(groups, {
1152
+ addLoadedSection("Extensions", () => formatCompactList(this.getCompactExtensionLabels(extensions)), () => this.formatScopeGroups(groups, {
1071
1153
  formatPath: (item) => this.formatExtensionDisplayPath(item.path),
1072
1154
  formatPackagePath: (item) => this.formatExtensionDisplayPath(this.getShortPath(item.path, item.sourceInfo)),
1073
- });
1074
- const extensionCompactList = formatCompactList(this.getCompactExtensionLabels(extensions));
1075
- addLoadedSection("Extensions", extensionCompactList, extList, "mdHeading");
1155
+ }), "mdHeading");
1076
1156
  }
1077
1157
  // Show loaded themes (excluding built-in)
1078
1158
  const loadedThemes = themesResult.themes;
@@ -1082,27 +1162,24 @@ export class InteractiveMode {
1082
1162
  path: loadedTheme.sourcePath,
1083
1163
  sourceInfo: loadedTheme.sourceInfo,
1084
1164
  })));
1085
- const themeList = this.formatScopeGroups(groups, {
1165
+ addLoadedSection("Themes", () => formatCompactList(customThemes.map((loadedTheme) => loadedTheme.name ??
1166
+ this.getCompactPathLabel(loadedTheme.sourcePath, loadedTheme.sourceInfo))), () => this.formatScopeGroups(groups, {
1086
1167
  formatPath: (item) => this.formatDisplayPath(item.path),
1087
1168
  formatPackagePath: (item) => this.getShortPath(item.path, item.sourceInfo),
1088
- });
1089
- const themeCompactList = formatCompactList(customThemes.map((loadedTheme) => loadedTheme.name ?? this.getCompactPathLabel(loadedTheme.sourcePath, loadedTheme.sourceInfo)));
1090
- addLoadedSection("Themes", themeCompactList, themeList);
1169
+ }));
1091
1170
  }
1092
1171
  }
1093
1172
  if (showDiagnostics) {
1094
- const skillDiagnostics = skillsResult.diagnostics;
1095
- if (skillDiagnostics.length > 0) {
1096
- const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);
1097
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1098
- this.chatContainer.addChild(new Spacer(1));
1099
- }
1100
- const promptDiagnostics = promptsResult.diagnostics;
1101
- if (promptDiagnostics.length > 0) {
1102
- const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);
1103
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1104
- this.chatContainer.addChild(new Spacer(1));
1105
- }
1173
+ const addDiagnosticsSection = (name, diagnostics) => {
1174
+ if (diagnostics.length === 0) {
1175
+ return;
1176
+ }
1177
+ addLoadedSection(name, () => this.formatDiagnosticsSummary(diagnostics), () => this.formatDiagnostics(diagnostics, sourceInfos), "warning");
1178
+ };
1179
+ const skillDiagnostics = diagnosticsForStartup(skillsResult.diagnostics);
1180
+ addDiagnosticsSection("Skill conflicts", skillDiagnostics);
1181
+ const promptDiagnostics = diagnosticsForStartup(promptsResult.diagnostics);
1182
+ addDiagnosticsSection("Prompt conflicts", promptDiagnostics);
1106
1183
  const extensionDiagnostics = [];
1107
1184
  const extensionErrors = this.session.resourceLoader.getExtensions().errors;
1108
1185
  if (extensionErrors.length > 0) {
@@ -1115,17 +1192,9 @@ export class InteractiveMode {
1115
1192
  extensionDiagnostics.push(...this.getBuiltInCommandConflictDiagnostics(this.session.extensionRunner));
1116
1193
  const shortcutDiagnostics = this.session.extensionRunner.getShortcutDiagnostics();
1117
1194
  extensionDiagnostics.push(...shortcutDiagnostics);
1118
- if (extensionDiagnostics.length > 0) {
1119
- const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);
1120
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1121
- this.chatContainer.addChild(new Spacer(1));
1122
- }
1123
- const themeDiagnostics = themesResult.diagnostics;
1124
- if (themeDiagnostics.length > 0) {
1125
- const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);
1126
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
1127
- this.chatContainer.addChild(new Spacer(1));
1128
- }
1195
+ addDiagnosticsSection("Extension issues", extensionDiagnostics);
1196
+ const themeDiagnostics = diagnosticsForStartup(themesResult.diagnostics);
1197
+ addDiagnosticsSection("Theme conflicts", themeDiagnostics);
1129
1198
  }
1130
1199
  }
1131
1200
  /**
@@ -1999,6 +2068,12 @@ export class InteractiveMode {
1999
2068
  this.editor.setText("");
2000
2069
  return;
2001
2070
  }
2071
+ if (text === "/think" || text.startsWith("/think ")) {
2072
+ const level = text.startsWith("/think ") ? text.slice(7).trim() : undefined;
2073
+ this.editor.setText("");
2074
+ this.handleThinkCommand(level);
2075
+ return;
2076
+ }
2002
2077
  if (text === "/scoped-models") {
2003
2078
  this.editor.setText("");
2004
2079
  await this.showModelsSelector();
@@ -2091,6 +2166,16 @@ export class InteractiveMode {
2091
2166
  await this.handleReloadCommand();
2092
2167
  return;
2093
2168
  }
2169
+ if (text === "/mcp") {
2170
+ this.editor.setText("");
2171
+ this.handleMcpCommand();
2172
+ return;
2173
+ }
2174
+ if (text === "/skills") {
2175
+ this.editor.setText("");
2176
+ this.handleSkillsCommand();
2177
+ return;
2178
+ }
2094
2179
  if (text === "/debug") {
2095
2180
  this.handleDebugCommand();
2096
2181
  this.editor.setText("");
@@ -3310,12 +3395,7 @@ export class InteractiveMode {
3310
3395
  this.updateEditorBorderColor();
3311
3396
  },
3312
3397
  onThemeChange: (themeName) => {
3313
- const result = setTheme(themeName, true);
3314
- this.settingsManager.setTheme(themeName);
3315
- this.ui.invalidate();
3316
- if (!result.success) {
3317
- this.showError(`Failed to load theme "${themeName}": ${result.error}\nFell back to dark theme.`);
3318
- }
3398
+ this.applySettingsThemeChange(themeName);
3319
3399
  },
3320
3400
  onThemePreview: (themeName) => {
3321
3401
  const result = setTheme(themeName, true);
@@ -3386,6 +3466,70 @@ export class InteractiveMode {
3386
3466
  return { component: selector, focus: selector.getSettingsList() };
3387
3467
  });
3388
3468
  }
3469
+ applySettingsThemeChange(themeName) {
3470
+ const previousTheme = this.settingsManager.getTheme?.() || "dark";
3471
+ let applyResult = {
3472
+ success: false,
3473
+ error: "theme transaction did not run",
3474
+ };
3475
+ const transaction = runHarnessControlTransactionSync({
3476
+ kind: "interactive.theme.apply",
3477
+ data: { themeName, previousTheme, persistenceScope: "default" },
3478
+ beforeState: { theme: previousTheme },
3479
+ afterState: () => ({ theme: themeName }),
3480
+ commit: () => {
3481
+ const result = setTheme(themeName, true);
3482
+ applyResult = result;
3483
+ if (!result.success) {
3484
+ throw new Error(result.error ?? `Failed to load theme "${themeName}"`);
3485
+ }
3486
+ this.settingsManager.setTheme(themeName);
3487
+ this.ui.invalidate();
3488
+ return { theme: themeName };
3489
+ },
3490
+ rollback: () => {
3491
+ setTheme(previousTheme, true);
3492
+ },
3493
+ });
3494
+ if (transaction.status !== "completed") {
3495
+ this.showError(`Failed to load theme "${themeName}": ${applyResult.error}\nRestored theme "${previousTheme}".`);
3496
+ return applyResult;
3497
+ }
3498
+ return applyResult;
3499
+ }
3500
+ handleThinkCommand(level) {
3501
+ if (!level) {
3502
+ this.showThinkingSelector();
3503
+ return;
3504
+ }
3505
+ const availableLevels = this.session.getAvailableThinkingLevels();
3506
+ if (!availableLevels.includes(level)) {
3507
+ this.showError(`Thinking level "${level}" is not available. Available: ${availableLevels.join(", ")}`);
3508
+ return;
3509
+ }
3510
+ this.applyThinkingLevel(level);
3511
+ }
3512
+ applyThinkingLevel(level) {
3513
+ const previousThinkingLevel = this.session.thinkingLevel;
3514
+ const transaction = runHarnessControlTransactionSync({
3515
+ kind: "interactive.model_thinking.commit",
3516
+ data: { thinkingLevel: level, previousThinkingLevel, persistenceScope: "session" },
3517
+ beforeState: { thinkingLevel: previousThinkingLevel },
3518
+ afterState: () => ({ thinkingLevel: level }),
3519
+ commit: () => {
3520
+ this.session.setThinkingLevel(level);
3521
+ this.footer.invalidate();
3522
+ this.updateEditorBorderColor();
3523
+ return { thinkingLevel: level };
3524
+ },
3525
+ rollback: () => {
3526
+ this.session.setThinkingLevel(previousThinkingLevel);
3527
+ },
3528
+ });
3529
+ if (transaction.status === "completed") {
3530
+ this.showStatus(`Thinking: ${this.session.thinkingLevel}`);
3531
+ }
3532
+ }
3389
3533
  async handleModelCommand(searchTerm) {
3390
3534
  if (!searchTerm) {
3391
3535
  this.showModelSelector();
@@ -3393,17 +3537,7 @@ export class InteractiveMode {
3393
3537
  }
3394
3538
  const model = await this.findExactModelMatch(searchTerm);
3395
3539
  if (model) {
3396
- try {
3397
- await this.session.setModel(model);
3398
- this.footer.invalidate();
3399
- this.updateEditorBorderColor();
3400
- this.showStatus(`Model: ${model.id}`);
3401
- void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
3402
- this.checkDaxnutsEasterEgg(model);
3403
- }
3404
- catch (error) {
3405
- this.showError(error instanceof Error ? error.message : String(error));
3406
- }
3540
+ this.showModelThinkingTransaction(model);
3407
3541
  return;
3408
3542
  }
3409
3543
  this.showModelSelector(searchTerm);
@@ -3458,22 +3592,102 @@ export class InteractiveMode {
3458
3592
  // Ignore auth lookup failures for warning-only checks.
3459
3593
  }
3460
3594
  }
3461
- showModelSelector(initialSearchInput) {
3595
+ showThinkingSelector() {
3462
3596
  this.showSelector((done) => {
3463
- const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
3464
- try {
3465
- await this.session.setModel(model);
3466
- this.footer.invalidate();
3467
- this.updateEditorBorderColor();
3597
+ const selector = new ThinkingSelectorComponent({
3598
+ thinkingLevel: this.session.thinkingLevel,
3599
+ availableThinkingLevels: this.session.getAvailableThinkingLevels(),
3600
+ }, {
3601
+ onThinkingLevelChange: (level) => {
3602
+ this.applyThinkingLevel(level);
3603
+ },
3604
+ onSelectComplete: () => {
3468
3605
  done();
3469
- this.showStatus(`Model: ${model.id}`);
3470
- void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
3471
- this.checkDaxnutsEasterEgg(model);
3472
- }
3473
- catch (error) {
3606
+ this.ui.requestRender();
3607
+ },
3608
+ onCancel: () => {
3474
3609
  done();
3475
- this.showError(error instanceof Error ? error.message : String(error));
3610
+ this.ui.requestRender();
3611
+ },
3612
+ });
3613
+ return { component: selector, focus: selector };
3614
+ });
3615
+ }
3616
+ showModelThinkingTransaction(model) {
3617
+ const availableThinkingLevels = getSupportedThinkingLevels(model);
3618
+ const thinkingLevel = clampThinkingLevel(model, this.session.thinkingLevel);
3619
+ // Auto-skip selector when the model offers at most one thinking level (T04 routing safeguard).
3620
+ if (availableThinkingLevels.length <= 1) {
3621
+ const chosen = (availableThinkingLevels[0] ?? thinkingLevel ?? "off");
3622
+ void this.commitModelThinkingSelection(model, chosen);
3623
+ return;
3624
+ }
3625
+ this.showSelector((done) => {
3626
+ const selector = new ThinkingSelectorComponent({
3627
+ thinkingLevel,
3628
+ availableThinkingLevels,
3629
+ }, {
3630
+ onThinkingLevelChange: () => { },
3631
+ onSelectComplete: (level) => {
3632
+ done();
3633
+ void this.commitModelThinkingSelection(model, level);
3634
+ },
3635
+ onCancel: () => {
3636
+ done();
3637
+ this.ui.requestRender();
3638
+ },
3639
+ });
3640
+ return { component: selector, focus: selector };
3641
+ });
3642
+ }
3643
+ async commitModelThinkingSelection(model, thinkingLevel) {
3644
+ const previousModel = this.session.model;
3645
+ const previousThinkingLevel = this.session.thinkingLevel;
3646
+ const transaction = await runHarnessControlTransaction({
3647
+ kind: "interactive.model_thinking.commit",
3648
+ data: {
3649
+ provider: model.provider,
3650
+ model: model.id,
3651
+ thinkingLevel,
3652
+ previousProvider: previousModel?.provider,
3653
+ previousModel: previousModel?.id,
3654
+ previousThinkingLevel,
3655
+ persistenceScope: "session",
3656
+ },
3657
+ beforeState: {
3658
+ provider: previousModel?.provider,
3659
+ model: previousModel?.id,
3660
+ thinkingLevel: previousThinkingLevel,
3661
+ },
3662
+ afterState: () => ({ provider: model.provider, model: model.id, thinkingLevel }),
3663
+ commit: async () => {
3664
+ await this.session.setModel(model);
3665
+ this.session.setThinkingLevel(thinkingLevel);
3666
+ this.footer.invalidate();
3667
+ this.updateEditorBorderColor();
3668
+ return { provider: model.provider, model: model.id, thinkingLevel };
3669
+ },
3670
+ rollback: async () => {
3671
+ if (previousModel && !modelsAreEqual(this.session.model, previousModel)) {
3672
+ await this.session.setModel(previousModel);
3476
3673
  }
3674
+ this.session.setThinkingLevel(previousThinkingLevel);
3675
+ },
3676
+ });
3677
+ if (transaction.status === "completed") {
3678
+ this.showStatus(`Model: ${model.id} · Thinking: ${this.session.thinkingLevel}`);
3679
+ void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
3680
+ this.checkDaxnutsEasterEgg(model);
3681
+ return;
3682
+ }
3683
+ const error = transaction.error ?? transaction.rollbackError ?? "Model transaction failed";
3684
+ this.showError(error instanceof Error ? error.message : String(error));
3685
+ }
3686
+ showModelSelector(initialSearchInput) {
3687
+ this.showSelector((done) => {
3688
+ const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, (model) => {
3689
+ done();
3690
+ this.showModelThinkingTransaction(model);
3477
3691
  }, () => {
3478
3692
  done();
3479
3693
  this.ui.requestRender();
@@ -4405,6 +4619,35 @@ export class InteractiveMode {
4405
4619
  this.chatContainer.addChild(new Text(info, 1, 0));
4406
4620
  this.ui.requestRender();
4407
4621
  }
4622
+ handleMcpCommand() {
4623
+ const inventory = loadMcpInventory(this.sessionManager.getCwd());
4624
+ this.showSelector((done) => {
4625
+ const selector = new McpSelectorComponent(inventory, () => {
4626
+ done();
4627
+ this.ui.requestRender();
4628
+ });
4629
+ return { component: selector, focus: selector };
4630
+ });
4631
+ const summary = inventory.entries.length === 1 ? "1 MCP server" : `${inventory.entries.length} MCP servers`;
4632
+ this.showStatus(`${summary} loaded (env values hidden)`);
4633
+ }
4634
+ handleSkillsCommand() {
4635
+ const result = this.session.resourceLoader.getSkills();
4636
+ const enableSkillCommands = this.settingsManager.getEnableSkillCommands();
4637
+ this.showSelector((done) => {
4638
+ const selector = new SkillsSelectorComponent({
4639
+ skills: result.skills,
4640
+ diagnostics: result.diagnostics,
4641
+ enableSkillCommands,
4642
+ }, () => {
4643
+ done();
4644
+ this.ui.requestRender();
4645
+ });
4646
+ return { component: selector, focus: selector };
4647
+ });
4648
+ const summary = result.skills.length === 1 ? "1 skill" : `${result.skills.length} skills`;
4649
+ this.showStatus(`${summary} loaded`);
4650
+ }
4408
4651
  handleChangelogCommand() {
4409
4652
  const changelogPath = getChangelogPath();
4410
4653
  const allEntries = parseChangelog(changelogPath);