mastracode 0.4.0 → 0.5.0

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 (155) hide show
  1. package/CHANGELOG.md +328 -0
  2. package/LICENSE.md +15 -0
  3. package/README.md +68 -29
  4. package/dist/agents/memory.d.ts.map +1 -1
  5. package/dist/agents/model.d.ts +17 -6
  6. package/dist/agents/model.d.ts.map +1 -1
  7. package/dist/agents/prompts/index.d.ts.map +1 -1
  8. package/dist/agents/prompts/tool-guidance.d.ts +2 -0
  9. package/dist/agents/prompts/tool-guidance.d.ts.map +1 -1
  10. package/dist/agents/subagents/audit-tests.d.ts +0 -7
  11. package/dist/agents/subagents/audit-tests.d.ts.map +1 -1
  12. package/dist/agents/subagents/execute.d.ts +0 -7
  13. package/dist/agents/subagents/execute.d.ts.map +1 -1
  14. package/dist/agents/subagents/explore.d.ts +0 -7
  15. package/dist/agents/subagents/explore.d.ts.map +1 -1
  16. package/dist/agents/subagents/index.d.ts.map +1 -1
  17. package/dist/agents/subagents/plan.d.ts +0 -7
  18. package/dist/agents/subagents/plan.d.ts.map +1 -1
  19. package/dist/agents/tools.d.ts +3 -1
  20. package/dist/agents/tools.d.ts.map +1 -1
  21. package/dist/agents/workspace.d.ts +4 -1
  22. package/dist/agents/workspace.d.ts.map +1 -1
  23. package/dist/{chunk-K4WJUBEC.cjs → chunk-AJEYT7X3.cjs} +763 -429
  24. package/dist/chunk-AJEYT7X3.cjs.map +1 -0
  25. package/dist/{chunk-U5A7TFNT.js → chunk-CC2724NI.js} +46 -10
  26. package/dist/chunk-CC2724NI.js.map +1 -0
  27. package/dist/{chunk-REVOTI2T.js → chunk-JI4M5525.js} +740 -412
  28. package/dist/chunk-JI4M5525.js.map +1 -0
  29. package/dist/{chunk-Z4QRXVST.cjs → chunk-MBPGUMYQ.cjs} +325 -251
  30. package/dist/chunk-MBPGUMYQ.cjs.map +1 -0
  31. package/dist/{chunk-MT3YCFCC.cjs → chunk-OEDRHUU5.cjs} +47 -9
  32. package/dist/chunk-OEDRHUU5.cjs.map +1 -0
  33. package/dist/{chunk-M5LKPQB4.js → chunk-WKPHD54B.js} +283 -209
  34. package/dist/chunk-WKPHD54B.js.map +1 -0
  35. package/dist/{chunk-C4X3C2DL.cjs → chunk-XVYUS2EA.cjs} +2213 -1035
  36. package/dist/chunk-XVYUS2EA.cjs.map +1 -0
  37. package/dist/{chunk-X3BGE7CL.js → chunk-YQNZ7DHQ.js} +1788 -613
  38. package/dist/chunk-YQNZ7DHQ.js.map +1 -0
  39. package/dist/cli.cjs +79 -31
  40. package/dist/cli.cjs.map +1 -1
  41. package/dist/cli.js +71 -23
  42. package/dist/cli.js.map +1 -1
  43. package/dist/clipboard/index.d.ts +5 -0
  44. package/dist/clipboard/index.d.ts.map +1 -1
  45. package/dist/error-classification.d.ts +10 -0
  46. package/dist/error-classification.d.ts.map +1 -0
  47. package/dist/index.cjs +2 -2
  48. package/dist/index.d.ts +10 -3
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/mcp/config.d.ts +8 -0
  52. package/dist/mcp/config.d.ts.map +1 -1
  53. package/dist/mcp/index.d.ts +1 -1
  54. package/dist/mcp/index.d.ts.map +1 -1
  55. package/dist/mcp/manager.d.ts +4 -2
  56. package/dist/mcp/manager.d.ts.map +1 -1
  57. package/dist/mcp/types.d.ts +30 -3
  58. package/dist/mcp/types.d.ts.map +1 -1
  59. package/dist/onboarding/onboarding-inline.d.ts +2 -0
  60. package/dist/onboarding/onboarding-inline.d.ts.map +1 -1
  61. package/dist/onboarding/packs.d.ts +1 -0
  62. package/dist/onboarding/packs.d.ts.map +1 -1
  63. package/dist/onboarding/settings.d.ts +37 -2
  64. package/dist/onboarding/settings.d.ts.map +1 -1
  65. package/dist/permissions-S3LGXIDB.js +3 -0
  66. package/dist/{permissions-CVXKYIWR.js.map → permissions-S3LGXIDB.js.map} +1 -1
  67. package/dist/permissions-VGABAVGD.cjs +40 -0
  68. package/dist/{permissions-2HIUSRQN.cjs.map → permissions-VGABAVGD.cjs.map} +1 -1
  69. package/dist/permissions.d.ts.map +1 -1
  70. package/dist/providers/claude-max.d.ts +13 -0
  71. package/dist/providers/claude-max.d.ts.map +1 -1
  72. package/dist/providers/openai-codex.d.ts +1 -0
  73. package/dist/providers/openai-codex.d.ts.map +1 -1
  74. package/dist/tool-names.d.ts +68 -0
  75. package/dist/tool-names.d.ts.map +1 -0
  76. package/dist/tools/ast-smart-edit.d.ts +77 -5
  77. package/dist/tools/ast-smart-edit.d.ts.map +1 -1
  78. package/dist/tools/index.d.ts +2 -2
  79. package/dist/tools/index.d.ts.map +1 -1
  80. package/dist/tools/string-replace-lsp.d.ts +15 -0
  81. package/dist/tools/string-replace-lsp.d.ts.map +1 -1
  82. package/dist/tools/subagent.d.ts.map +1 -1
  83. package/dist/tools/utils.d.ts +4 -2
  84. package/dist/tools/utils.d.ts.map +1 -1
  85. package/dist/tui/command-dispatch.d.ts.map +1 -1
  86. package/dist/tui/commands/clone.d.ts +29 -0
  87. package/dist/tui/commands/clone.d.ts.map +1 -0
  88. package/dist/tui/commands/custom-providers.d.ts +8 -0
  89. package/dist/tui/commands/custom-providers.d.ts.map +1 -0
  90. package/dist/tui/commands/index.d.ts +3 -1
  91. package/dist/tui/commands/index.d.ts.map +1 -1
  92. package/dist/tui/commands/mcp.d.ts.map +1 -1
  93. package/dist/tui/commands/models-pack.d.ts +4 -0
  94. package/dist/tui/commands/models-pack.d.ts.map +1 -1
  95. package/dist/tui/commands/om.d.ts.map +1 -1
  96. package/dist/tui/commands/report-issue.d.ts +3 -0
  97. package/dist/tui/commands/report-issue.d.ts.map +1 -0
  98. package/dist/tui/commands/resource.d.ts.map +1 -1
  99. package/dist/tui/commands/settings.d.ts.map +1 -1
  100. package/dist/tui/commands/threads.d.ts +1 -0
  101. package/dist/tui/commands/threads.d.ts.map +1 -1
  102. package/dist/tui/components/ask-question-inline.d.ts +3 -0
  103. package/dist/tui/components/ask-question-inline.d.ts.map +1 -1
  104. package/dist/tui/components/custom-editor.d.ts +1 -1
  105. package/dist/tui/components/custom-editor.d.ts.map +1 -1
  106. package/dist/tui/components/help-overlay.d.ts.map +1 -1
  107. package/dist/tui/components/plan-approval-inline.d.ts.map +1 -1
  108. package/dist/tui/components/settings.d.ts +2 -0
  109. package/dist/tui/components/settings.d.ts.map +1 -1
  110. package/dist/tui/components/subagent-execution.d.ts +6 -1
  111. package/dist/tui/components/subagent-execution.d.ts.map +1 -1
  112. package/dist/tui/components/thread-selector.d.ts +6 -0
  113. package/dist/tui/components/thread-selector.d.ts.map +1 -1
  114. package/dist/tui/components/tool-execution-enhanced.d.ts +1 -0
  115. package/dist/tui/components/tool-execution-enhanced.d.ts.map +1 -1
  116. package/dist/tui/components/tool-validation-error.d.ts.map +1 -1
  117. package/dist/tui/handlers/message.d.ts.map +1 -1
  118. package/dist/tui/handlers/prompts.d.ts +6 -0
  119. package/dist/tui/handlers/prompts.d.ts.map +1 -1
  120. package/dist/tui/handlers/subagent.d.ts.map +1 -1
  121. package/dist/tui/mastra-tui.d.ts +14 -5
  122. package/dist/tui/mastra-tui.d.ts.map +1 -1
  123. package/dist/tui/render-messages.d.ts.map +1 -1
  124. package/dist/tui/setup.d.ts.map +1 -1
  125. package/dist/tui/state.d.ts +4 -5
  126. package/dist/tui/state.d.ts.map +1 -1
  127. package/dist/tui.cjs +19 -19
  128. package/dist/tui.js +2 -2
  129. package/dist/utils/debug-log.d.ts +12 -0
  130. package/dist/utils/debug-log.d.ts.map +1 -0
  131. package/dist/utils/plans.d.ts +7 -0
  132. package/dist/utils/plans.d.ts.map +1 -0
  133. package/dist/utils/update-check.d.ts +40 -0
  134. package/dist/utils/update-check.d.ts.map +1 -0
  135. package/package.json +8 -8
  136. package/dist/chunk-C4X3C2DL.cjs.map +0 -1
  137. package/dist/chunk-K4WJUBEC.cjs.map +0 -1
  138. package/dist/chunk-M5LKPQB4.js.map +0 -1
  139. package/dist/chunk-MT3YCFCC.cjs.map +0 -1
  140. package/dist/chunk-REVOTI2T.js.map +0 -1
  141. package/dist/chunk-U5A7TFNT.js.map +0 -1
  142. package/dist/chunk-X3BGE7CL.js.map +0 -1
  143. package/dist/chunk-Z4QRXVST.cjs.map +0 -1
  144. package/dist/docs/SKILL.md +0 -30
  145. package/dist/docs/assets/SOURCE_MAP.json +0 -11
  146. package/dist/docs/references/docs-mastra-code-configuration.md +0 -299
  147. package/dist/docs/references/docs-mastra-code-customization.md +0 -228
  148. package/dist/docs/references/docs-mastra-code-modes.md +0 -104
  149. package/dist/docs/references/docs-mastra-code-overview.md +0 -135
  150. package/dist/docs/references/docs-mastra-code-tools.md +0 -229
  151. package/dist/docs/references/reference-mastra-code-createMastraCode.md +0 -108
  152. package/dist/permissions-2HIUSRQN.cjs +0 -40
  153. package/dist/permissions-CVXKYIWR.js +0 -3
  154. package/dist/tui/commands/models.d.ts +0 -3
  155. package/dist/tui/commands/models.d.ts.map +0 -1
@@ -1,22 +1,133 @@
1
- import { theme, mastra, getMarkdownTheme, getEditorTheme, loadSettings, saveSettings, getAvailableModePacks, getAvailableOmPacks, ONBOARDING_VERSION, ThreadLockError, parseSubagentMeta, tintHex, getSelectListTheme, getThemeMode, applyThemeMode, getSettingsListTheme } from './chunk-REVOTI2T.js';
2
- import { getOAuthProviders, detectProject, getUserId, getCurrentGitBranch, PROVIDER_DEFAULT_MODELS } from './chunk-SM3QCOA7.js';
3
- import { getToolCategory, TOOL_CATEGORIES } from './chunk-U5A7TFNT.js';
1
+ import { theme, mastra, getMarkdownTheme, getEditorTheme, loadSettings, getAvailableModePacks, resolveThreadActiveModelPackId, saveSettings, getAvailableOmPacks, ONBOARDING_VERSION, THREAD_ACTIVE_MODEL_PACK_ID_KEY, ThreadLockError, parseSubagentMeta, tintHex, getSelectListTheme, getThemeMode, applyThemeMode, getCustomProviderId, getSettingsListTheme, toCustomProviderModelId } from './chunk-JI4M5525.js';
2
+ import { getOAuthProviders, detectProject, getUserId, getCurrentGitBranch, getAppDataDir, PROVIDER_DEFAULT_MODELS } from './chunk-SM3QCOA7.js';
3
+ import { MC_TOOLS, getToolCategory, TOOL_CATEGORIES } from './chunk-CC2724NI.js';
4
4
  import { Box, Text, Spacer, Input, Container, fuzzyFilter, getEditorKeybindings, Markdown, ProcessTerminal, TUI, Editor, matchesKey, CombinedAutocompleteProvider, SelectList, visibleWidth, SettingsList, isKeyRelease } from '@mariozechner/pi-tui';
5
5
  import chalk10 from 'chalk';
6
- import { exec, execFileSync, execSync } from 'child_process';
7
- import fs2, { readFileSync, unlinkSync, promises } from 'fs';
8
- import * as path4 from 'path';
9
- import path4__default, { join } from 'path';
6
+ import { exec, execFile, execSync, execFileSync } from 'child_process';
7
+ import fs2, { realpathSync, readFileSync, unlinkSync, promises } from 'fs';
8
+ import { createRequire } from 'module';
9
+ import * as path5 from 'path';
10
+ import path5__default, { join } from 'path';
10
11
  import { defaultOMProgressState } from '@mastra/core/harness';
11
12
  import * as os from 'os';
12
13
  import { tmpdir } from 'os';
13
14
  import { highlight } from 'cli-highlight';
15
+ import fs4 from 'fs/promises';
14
16
  import { parse } from 'partial-json';
15
17
  import * as yaml from 'js-yaml';
16
18
 
17
19
  // src/auth/claude-max-warning.ts
18
20
  var ANTHROPIC_OAUTH_PROVIDER_ID = "anthropic";
19
21
  var CLAUDE_MAX_OAUTH_WARNING_MESSAGE = "OAuth with a Claude Max plan is a grey area. Anthropic has reportedly banned users for using Claude max credentials outside of Claude Code, and it may violate Anthropic Terms of Service. Proceed at your own risk.";
22
+ var AskQuestionInlineComponent = class extends Container {
23
+ contentBox;
24
+ selectList;
25
+ input;
26
+ onSubmit;
27
+ onCancel;
28
+ formatResult;
29
+ isNegativeAnswer;
30
+ allowEmptyInput = false;
31
+ answered = false;
32
+ questionText;
33
+ _focused = false;
34
+ get focused() {
35
+ return this._focused;
36
+ }
37
+ set focused(value) {
38
+ this._focused = value;
39
+ if (!this.answered && this.input) {
40
+ this.input.focused = value;
41
+ }
42
+ }
43
+ constructor(options, _ui) {
44
+ super();
45
+ this.onSubmit = options.onSubmit;
46
+ this.onCancel = options.onCancel;
47
+ this.formatResult = options.formatResult;
48
+ this.isNegativeAnswer = options.isNegativeAnswer;
49
+ this.allowEmptyInput = Boolean(options.allowEmptyInput);
50
+ this.questionText = options.question;
51
+ this.addChild(new Spacer(1));
52
+ this.contentBox = new Box(1, 1, (text) => theme.bg("toolPendingBg", text));
53
+ this.addChild(this.contentBox);
54
+ this.contentBox.addChild(new Text(theme.bold(theme.fg("accent", "\u2753 Question")), 0, 0));
55
+ this.contentBox.addChild(new Spacer(1));
56
+ for (const line of options.question.split("\n")) {
57
+ this.contentBox.addChild(new Text(theme.fg("text", line), 0, 0));
58
+ }
59
+ this.contentBox.addChild(new Spacer(1));
60
+ if (options.options && options.options.length > 0) {
61
+ this.buildSelectMode(options.options);
62
+ } else {
63
+ this.buildInputMode();
64
+ }
65
+ }
66
+ buildSelectMode(opts) {
67
+ const items = opts.map((opt) => ({
68
+ value: opt.label,
69
+ label: opt.description ? ` ${opt.label} ${theme.fg("dim", opt.description)}` : ` ${opt.label}`
70
+ }));
71
+ this.selectList = new SelectList(items, Math.min(items.length, 8), getSelectListTheme());
72
+ this.selectList.onSelect = (item) => {
73
+ this.handleAnswer(item.value);
74
+ };
75
+ this.selectList.onCancel = () => {
76
+ this.handleCancel();
77
+ };
78
+ this.contentBox.addChild(this.selectList);
79
+ this.contentBox.addChild(new Spacer(1));
80
+ this.contentBox.addChild(new Text(theme.fg("dim", "\u2191\u2193 to navigate \xB7 Enter to select \xB7 Esc to skip"), 0, 0));
81
+ }
82
+ buildInputMode() {
83
+ this.input = new Input();
84
+ this.input.onSubmit = (value) => {
85
+ const trimmed = value.trim();
86
+ if (trimmed || this.allowEmptyInput) {
87
+ this.handleAnswer(trimmed);
88
+ }
89
+ };
90
+ this.contentBox.addChild(this.input);
91
+ this.contentBox.addChild(new Spacer(1));
92
+ this.contentBox.addChild(new Text(theme.fg("dim", "Enter to submit \xB7 Esc to skip"), 0, 0));
93
+ }
94
+ handleAnswer(answer) {
95
+ if (this.answered) return;
96
+ this.answered = true;
97
+ const isNegative = this.isNegativeAnswer?.(answer) ?? false;
98
+ this.contentBox.clear();
99
+ this.contentBox.setBgFn((text) => theme.bg(isNegative ? "toolErrorBg" : "toolSuccessBg", text));
100
+ const resultText = this.formatResult ? this.formatResult(answer) : `${this.questionText} \u2192 ${answer}`;
101
+ const icon = isNegative ? theme.fg("error", "\u2717") : theme.fg("success", "\u2713");
102
+ this.contentBox.addChild(new Text(theme.fg("text", `${icon} ${resultText}`), 0, 0));
103
+ this.onSubmit(answer);
104
+ }
105
+ handleCancel() {
106
+ if (this.answered) return;
107
+ this.answered = true;
108
+ this.contentBox.clear();
109
+ this.contentBox.setBgFn((text) => theme.bg("toolErrorBg", text));
110
+ this.contentBox.addChild(
111
+ new Text(theme.fg("dim", `${theme.fg("error", "\u2717")} ${this.questionText} (cancelled)`), 0, 0)
112
+ );
113
+ this.onCancel();
114
+ }
115
+ handleInput(data) {
116
+ if (this.answered) return;
117
+ if (this.selectList) {
118
+ this.selectList.handleInput(data);
119
+ } else if (this.input) {
120
+ const kb = getEditorKeybindings();
121
+ if (kb.matches(data, "selectCancel")) {
122
+ this.handleCancel();
123
+ return;
124
+ }
125
+ this.input.handleInput(data);
126
+ }
127
+ }
128
+ };
129
+
130
+ // src/onboarding/onboarding-inline.ts
20
131
  var OnboardingInlineComponent = class extends Container {
21
132
  tui;
22
133
  options;
@@ -24,6 +135,7 @@ var OnboardingInlineComponent = class extends Container {
24
135
  currentStep = "welcome";
25
136
  stepBox;
26
137
  selectList;
138
+ activeInlineQuestion;
27
139
  _finished = false;
28
140
  // Collected choices
29
141
  loginRequested = false;
@@ -235,25 +347,22 @@ var OnboardingInlineComponent = class extends Container {
235
347
  modePackDetail;
236
348
  renderModePack() {
237
349
  const packs = this.options.modePacks;
350
+ const box = this.makeBox();
238
351
  if (!this.options.hasProviderAccess) {
239
- const box2 = this.makeBox();
240
- box2.addChild(new Text(theme.bold(theme.fg("error", "No model providers configured")), 0, 0));
241
- box2.addChild(new Spacer(1));
242
- box2.addChild(new Text(theme.fg("text", "To use Mastra Code you need at least one API key or OAuth login"), 0, 0));
243
- box2.addChild(new Text(theme.fg("text", "for Anthropic, OpenAI, or another supported provider."), 0, 0));
244
- box2.addChild(new Spacer(1));
245
- box2.addChild(
352
+ box.addChild(new Text(theme.bold(theme.fg("warning", "No model providers configured")), 0, 0));
353
+ box.addChild(new Spacer(1));
354
+ box.addChild(new Text(theme.fg("text", "To use Mastra Code you need at least one API key or OAuth login"), 0, 0));
355
+ box.addChild(new Text(theme.fg("text", "for Anthropic, OpenAI, or another supported provider."), 0, 0));
356
+ box.addChild(new Spacer(1));
357
+ box.addChild(
246
358
  new Text(theme.fg("dim", "See https://mastra.ai/models for supported providers and API key env vars."), 0, 0)
247
359
  );
248
- box2.addChild(new Spacer(1));
249
- box2.addChild(
360
+ box.addChild(new Spacer(1));
361
+ box.addChild(
250
362
  new Text(theme.fg("dim", "Set an API key and restart, or run /login to authenticate via OAuth."), 0, 0)
251
363
  );
252
- this._finished = true;
253
- setTimeout(() => process.exit(1), 3e3);
254
- return;
364
+ box.addChild(new Spacer(1));
255
365
  }
256
- const box = this.makeBox();
257
366
  box.addChild(new Text(theme.bold(theme.fg("accent", "Model Packs")), 0, 0));
258
367
  box.addChild(new Spacer(1));
259
368
  box.addChild(new Text(theme.fg("text", "Choose default models for each mode (build / plan / fast):"), 0, 0));
@@ -310,9 +419,42 @@ var OnboardingInlineComponent = class extends Container {
310
419
  // ---------------------------------------------------------------------------
311
420
  // Custom pack flow — sequential model selection for each mode
312
421
  // ---------------------------------------------------------------------------
422
+ async promptCustomPackName() {
423
+ return new Promise((resolve2) => {
424
+ const question = new AskQuestionInlineComponent(
425
+ {
426
+ question: "Name this custom pack",
427
+ formatResult: (answer) => `Custom pack: ${answer}`,
428
+ onSubmit: (answer) => {
429
+ this.activeInlineQuestion = void 0;
430
+ const trimmed = answer.trim();
431
+ resolve2(trimmed.length > 0 ? trimmed : null);
432
+ },
433
+ onCancel: () => {
434
+ this.activeInlineQuestion = void 0;
435
+ resolve2(null);
436
+ }
437
+ },
438
+ this.tui
439
+ );
440
+ this.activeInlineQuestion = question;
441
+ this.stepBox.addChild(new Spacer(1));
442
+ this.stepBox.addChild(question);
443
+ this.tui.requestRender();
444
+ });
445
+ }
313
446
  async runCustomPackFlow() {
314
447
  this.selectList = void 0;
315
- this.collapseStep("Model pack \u2192 Custom");
448
+ const packName = await this.promptCustomPackName();
449
+ if (!packName) {
450
+ const fallback = this.options.modePacks.find((p) => p.id !== "custom") ?? this.options.modePacks[0];
451
+ this.selectedModePack = fallback;
452
+ this.collapseStep(`Model pack \u2192 ${theme.bold(this.selectedModePack.name)} (cancelled custom)`);
453
+ this.renderStep("omPack");
454
+ this.tui.requestRender();
455
+ return;
456
+ }
457
+ this.collapseStep(`Model pack \u2192 Custom (${packName})`);
316
458
  const modes = [
317
459
  { id: "plan", label: "plan", color: mastra.blue },
318
460
  { id: "build", label: "build", color: mastra.purple },
@@ -333,13 +475,13 @@ var OnboardingInlineComponent = class extends Container {
333
475
  models[mode.id] = modelId;
334
476
  }
335
477
  this.selectedModePack = {
336
- id: "custom",
337
- name: "Custom",
338
- description: "User-selected models",
478
+ id: `custom:${packName}`,
479
+ name: packName,
480
+ description: "Saved custom pack",
339
481
  models: { build: models.build, plan: models.plan, fast: models.fast }
340
482
  };
341
483
  this.collapseStep(
342
- `Model pack \u2192 ${theme.bold("Custom")} ${chalk10.hex(mastra.blue)("plan")} ${models.plan} ${chalk10.hex(mastra.purple)("build")} ${models.build} ${chalk10.hex(mastra.green)("fast")} ${models.fast}`
484
+ `Model pack \u2192 ${theme.bold(packName)} ${chalk10.hex(mastra.blue)("plan")} ${models.plan} ${chalk10.hex(mastra.purple)("build")} ${models.build} ${chalk10.hex(mastra.green)("fast")} ${models.fast}`
343
485
  );
344
486
  this.renderStep("omPack");
345
487
  this.tui.requestRender();
@@ -481,125 +623,128 @@ var OnboardingInlineComponent = class extends Container {
481
623
  this.stepBox.setBgFn((text) => theme.bg("toolSuccessBg", text));
482
624
  this.stepBox.addChild(new Text(`${theme.fg("success", "\u2713")} ${theme.fg("text", summary)}`, 0, 0));
483
625
  this.selectList = void 0;
626
+ this.activeInlineQuestion = void 0;
484
627
  }
485
628
  // ---------------------------------------------------------------------------
486
629
  // Input handling
487
630
  // ---------------------------------------------------------------------------
488
631
  handleInput(data) {
489
632
  if (this._finished) return;
633
+ if (this.activeInlineQuestion) {
634
+ this.activeInlineQuestion.handleInput(data);
635
+ return;
636
+ }
490
637
  if (this.selectList) {
491
638
  this.selectList.handleInput(data);
492
639
  return;
493
640
  }
494
641
  }
495
642
  };
496
- var AskQuestionInlineComponent = class extends Container {
497
- contentBox;
498
- selectList;
499
- input;
500
- onSubmit;
501
- onCancel;
502
- formatResult;
503
- isNegativeAnswer;
504
- answered = false;
505
- questionText;
506
- _focused = false;
507
- get focused() {
508
- return this._focused;
643
+ var PACKAGE_NAME = "mastracode";
644
+ var NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
645
+ var FETCH_TIMEOUT_MS = 5e3;
646
+ function matchPM(str) {
647
+ if (/pnpm/i.test(str)) return "pnpm";
648
+ if (/\byarn\b/i.test(str)) return "yarn";
649
+ if (/\bbun\b/i.test(str)) return "bun";
650
+ if (/\bnpm\b/i.test(str)) return "npm";
651
+ return null;
652
+ }
653
+ async function detectPackageManager() {
654
+ const userAgent = process.env.npm_config_user_agent;
655
+ if (userAgent) {
656
+ const pm = matchPM(userAgent);
657
+ if (pm) return pm;
509
658
  }
510
- set focused(value) {
511
- this._focused = value;
512
- if (!this.answered && this.input) {
513
- this.input.focused = value;
514
- }
659
+ const execPath = process.env.npm_execpath;
660
+ if (execPath) {
661
+ const pm = matchPM(execPath);
662
+ if (pm) return pm;
515
663
  }
516
- constructor(options, _ui) {
517
- super();
518
- this.onSubmit = options.onSubmit;
519
- this.onCancel = options.onCancel;
520
- this.formatResult = options.formatResult;
521
- this.isNegativeAnswer = options.isNegativeAnswer;
522
- this.questionText = options.question;
523
- this.addChild(new Spacer(1));
524
- this.contentBox = new Box(1, 1, (text) => theme.bg("toolPendingBg", text));
525
- this.addChild(this.contentBox);
526
- this.contentBox.addChild(new Text(theme.bold(theme.fg("accent", "\u2753 Question")), 0, 0));
527
- this.contentBox.addChild(new Spacer(1));
528
- for (const line of options.question.split("\n")) {
529
- this.contentBox.addChild(new Text(theme.fg("text", line), 0, 0));
530
- }
531
- this.contentBox.addChild(new Spacer(1));
532
- if (options.options && options.options.length > 0) {
533
- this.buildSelectMode(options.options);
534
- } else {
535
- this.buildInputMode();
536
- }
537
- }
538
- buildSelectMode(opts) {
539
- const items = opts.map((opt) => ({
540
- value: opt.label,
541
- label: opt.description ? ` ${opt.label} ${theme.fg("dim", opt.description)}` : ` ${opt.label}`
542
- }));
543
- this.selectList = new SelectList(items, Math.min(items.length, 8), getSelectListTheme());
544
- this.selectList.onSelect = (item) => {
545
- this.handleAnswer(item.value);
546
- };
547
- this.selectList.onCancel = () => {
548
- this.handleCancel();
549
- };
550
- this.contentBox.addChild(this.selectList);
551
- this.contentBox.addChild(new Spacer(1));
552
- this.contentBox.addChild(new Text(theme.fg("dim", "\u2191\u2193 to navigate \xB7 Enter to select \xB7 Esc to skip"), 0, 0));
664
+ const nodePath = process.env.NODE_PATH;
665
+ if (nodePath) {
666
+ if (/[/\\]\.pnpm[/\\]/.test(nodePath) || /[/\\]pnpm[/\\]/.test(nodePath)) return "pnpm";
667
+ if (/[/\\]\.yarn[/\\]/.test(nodePath)) return "yarn";
668
+ if (/[/\\]\.bun[/\\]/.test(nodePath)) return "bun";
553
669
  }
554
- buildInputMode() {
555
- this.input = new Input();
556
- this.input.onSubmit = (value) => {
557
- const trimmed = value.trim();
558
- if (trimmed) {
559
- this.handleAnswer(trimmed);
560
- }
561
- };
562
- this.contentBox.addChild(this.input);
563
- this.contentBox.addChild(new Spacer(1));
564
- this.contentBox.addChild(new Text(theme.fg("dim", "Enter to submit \xB7 Esc to skip"), 0, 0));
670
+ try {
671
+ const scriptPath = realpathSync(process.argv[1] ?? "");
672
+ if (/[/\\]\.?pnpm[/\\]/.test(scriptPath)) return "pnpm";
673
+ if (/[/\\]\.?yarn[/\\]/.test(scriptPath)) return "yarn";
674
+ if (/[/\\]\.?bun[/\\]/.test(scriptPath)) return "bun";
675
+ } catch {
565
676
  }
566
- handleAnswer(answer) {
567
- if (this.answered) return;
568
- this.answered = true;
569
- const isNegative = this.isNegativeAnswer?.(answer) ?? false;
570
- this.contentBox.clear();
571
- this.contentBox.setBgFn((text) => theme.bg(isNegative ? "toolErrorBg" : "toolSuccessBg", text));
572
- const resultText = this.formatResult ? this.formatResult(answer) : `${this.questionText} \u2192 ${answer}`;
573
- const icon = isNegative ? theme.fg("error", "\u2717") : theme.fg("success", "\u2713");
574
- this.contentBox.addChild(new Text(theme.fg("text", `${icon} ${resultText}`), 0, 0));
575
- this.onSubmit(answer);
677
+ const pnpmResult = await new Promise((resolve2) => {
678
+ execFile("pnpm", ["list", "-g", "--depth=0", PACKAGE_NAME], { timeout: 3e3 }, (error, stdout) => {
679
+ resolve2(!error && stdout.includes(PACKAGE_NAME));
680
+ });
681
+ });
682
+ if (pnpmResult) return "pnpm";
683
+ return "npm";
684
+ }
685
+ function getInstallCommand(pm, version) {
686
+ const pkg = version ? `${PACKAGE_NAME}@${version}` : `${PACKAGE_NAME}@latest`;
687
+ switch (pm) {
688
+ case "pnpm":
689
+ return `pnpm add -g ${pkg}`;
690
+ case "yarn":
691
+ return `yarn global add ${pkg}`;
692
+ case "bun":
693
+ return `bun add -g ${pkg}`;
694
+ default:
695
+ return `npm install -g ${pkg}`;
576
696
  }
577
- handleCancel() {
578
- if (this.answered) return;
579
- this.answered = true;
580
- this.contentBox.clear();
581
- this.contentBox.setBgFn((text) => theme.bg("toolErrorBg", text));
582
- this.contentBox.addChild(
583
- new Text(theme.fg("dim", `${theme.fg("error", "\u2717")} ${this.questionText} (cancelled)`), 0, 0)
584
- );
585
- this.onCancel();
697
+ }
698
+ function getCurrentVersion() {
699
+ const require2 = createRequire(import.meta.url);
700
+ const pkg = require2("../../package.json");
701
+ return pkg.version;
702
+ }
703
+ async function fetchLatestVersion() {
704
+ try {
705
+ const controller = new AbortController();
706
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
707
+ const res = await fetch(NPM_REGISTRY_URL, { signal: controller.signal });
708
+ clearTimeout(timeout);
709
+ if (!res.ok) return null;
710
+ const data = await res.json();
711
+ return data.version ?? null;
712
+ } catch {
713
+ return null;
586
714
  }
587
- handleInput(data) {
588
- if (this.answered) return;
589
- if (this.selectList) {
590
- this.selectList.handleInput(data);
591
- } else if (this.input) {
592
- const kb = getEditorKeybindings();
593
- if (kb.matches(data, "selectCancel")) {
594
- this.handleCancel();
595
- return;
596
- }
597
- this.input.handleInput(data);
598
- }
715
+ }
716
+ function isNewerVersion(current, latest) {
717
+ const parse = (v) => v.replace(/^v/, "").split("-")[0].split(".").map((s) => {
718
+ const n = Number(s);
719
+ return Number.isFinite(n) ? n : 0;
720
+ });
721
+ const [cMajor = 0, cMinor = 0, cPatch = 0] = parse(current);
722
+ const [lMajor = 0, lMinor = 0, lPatch = 0] = parse(latest);
723
+ if (lMajor !== cMajor) return lMajor > cMajor;
724
+ if (lMinor !== cMinor) return lMinor > cMinor;
725
+ return lPatch > cPatch;
726
+ }
727
+ function runUpdate(pm, targetVersion) {
728
+ const args = buildInstallArgs(pm, targetVersion);
729
+ return new Promise((resolve2) => {
730
+ execFile(pm, args, { timeout: 6e4 }, (error) => {
731
+ resolve2(!error);
732
+ });
733
+ });
734
+ }
735
+ function buildInstallArgs(pm, version) {
736
+ const pkg = `${PACKAGE_NAME}@${version}`;
737
+ switch (pm) {
738
+ case "pnpm":
739
+ return ["add", "-g", pkg];
740
+ case "yarn":
741
+ return ["global", "add", pkg];
742
+ case "bun":
743
+ return ["add", "-g", pkg];
744
+ default:
745
+ return ["install", "-g", pkg];
599
746
  }
600
- };
601
-
602
- // src/tui/claude-max-warning.ts
747
+ }
603
748
  function showClaudeMaxOAuthWarning(state, mode) {
604
749
  const options = mode === "login" ? [
605
750
  { label: "Continue", description: "Proceed with Anthropic OAuth" },
@@ -695,7 +840,7 @@ async function replaceFileReferences(template, workingDir) {
695
840
  for (const match of matches) {
696
841
  const [fullMatch, filePath] = match;
697
842
  try {
698
- const fullPath = path4.resolve(workingDir, filePath);
843
+ const fullPath = path5.resolve(workingDir, filePath);
699
844
  const content = await promises.readFile(fullPath, "utf-8");
700
845
  result = result.replace(fullMatch, content);
701
846
  } catch (error) {
@@ -715,13 +860,14 @@ function getCommands(modes) {
715
860
  { key: "/name", description: "Rename current thread" },
716
861
  { key: "/resource", description: "Show/switch resource ID" },
717
862
  { key: "/skills", description: "List available skills" },
718
- { key: "/models", description: "Configure model" },
719
- { key: "/models:pack", description: "Switch model pack" },
863
+ { key: "/models", description: "Switch model pack" },
864
+ { key: "/custom-providers", description: "Manage custom providers and models" },
720
865
  { key: "/subagents", description: "Configure subagent models" },
721
866
  { key: "/permissions", description: "Tool approval permissions" },
722
867
  { key: "/settings", description: "Notifications, YOLO, thinking" },
723
868
  { key: "/om", description: "Configure Observational Memory" },
724
869
  { key: "/review", description: "Review a GitHub pull request" },
870
+ { key: "/report-issue", description: "Open or browse mastracode issues" },
725
871
  { key: "/cost", description: "Token usage and costs" },
726
872
  { key: "/diff", description: "Modified files or git diff" },
727
873
  { key: "/sandbox", description: "Manage sandbox allowed paths" },
@@ -748,7 +894,8 @@ function getShortcuts(modes) {
748
894
  { key: "Ctrl+T", description: "Toggle thinking blocks" },
749
895
  { key: "Ctrl+E", description: "Expand/collapse tool outputs" },
750
896
  { key: "Ctrl+Y", description: "Toggle YOLO mode" },
751
- { key: "Ctrl+Z", description: "Undo last clear" }
897
+ { key: "Ctrl+Z", description: "Suspend process (fg to resume)" },
898
+ { key: "Alt+Z", description: "Undo last clear" }
752
899
  ];
753
900
  if (modes > 1) {
754
901
  shortcuts.push({ key: "\u21E7+Tab", description: "Cycle agent modes" });
@@ -957,7 +1104,7 @@ async function handlePermissionsCommand(ctx, args) {
957
1104
  await showPermissions(ctx);
958
1105
  }
959
1106
  async function showPermissions(ctx) {
960
- const { TOOL_CATEGORIES: TOOL_CATEGORIES2, getToolsForCategory } = await import('./permissions-CVXKYIWR.js');
1107
+ const { TOOL_CATEGORIES: TOOL_CATEGORIES2, getToolsForCategory } = await import('./permissions-S3LGXIDB.js');
961
1108
  const rules = ctx.harness.getPermissionRules();
962
1109
  const grants = ctx.harness.getSessionGrants();
963
1110
  const isYolo = ctx.harness.getState().yolo === true;
@@ -1116,13 +1263,21 @@ Example mcp.json:
1116
1263
  "command": "npx",
1117
1264
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"],
1118
1265
  "env": {}
1266
+ },
1267
+ "remote-api": {
1268
+ "url": "https://mcp.example.com/sse",
1269
+ "headers": { "Authorization": "Bearer <token>" }
1119
1270
  }
1120
1271
  }
1121
- }`
1272
+ }
1273
+
1274
+ Note: For dynamic auth (token refresh), use a stdio wrapper.
1275
+ "headers" only supports static values.`
1122
1276
  );
1123
1277
  return;
1124
1278
  }
1125
1279
  const statuses = mm.getServerStatuses();
1280
+ const skipped = mm.getSkippedServers();
1126
1281
  const lines = [`MCP Servers:`];
1127
1282
  lines.push(` Project: ${paths.project}`);
1128
1283
  lines.push(` Global: ${paths.global}`);
@@ -1131,13 +1286,20 @@ Example mcp.json:
1131
1286
  for (const status of statuses) {
1132
1287
  const icon = status.connected ? "\u2713" : "\u2717";
1133
1288
  const state = status.connected ? "connected" : `error: ${status.error}`;
1134
- lines.push(` ${icon} ${status.name} (${state})`);
1289
+ lines.push(` ${icon} ${status.name} [${status.transport}] (${state})`);
1135
1290
  if (status.toolNames.length > 0) {
1136
1291
  for (const toolName of status.toolNames) {
1137
1292
  lines.push(` - ${toolName}`);
1138
1293
  }
1139
1294
  }
1140
1295
  }
1296
+ if (skipped.length > 0) {
1297
+ lines.push("");
1298
+ lines.push(" Skipped:");
1299
+ for (const s of skipped) {
1300
+ lines.push(` \u2717 ${s.name}: ${s.reason}`);
1301
+ }
1302
+ }
1141
1303
  lines.push("");
1142
1304
  lines.push(` /mcp reload - Disconnect and reconnect all servers`);
1143
1305
  ctx.showInfo(lines.join("\n"));
@@ -1203,11 +1365,80 @@ Skills are automatically activated by the agent when relevant.`
1203
1365
  ctx.showError(`Failed to list skills: ${error instanceof Error ? error.message : String(error)}`);
1204
1366
  }
1205
1367
  }
1206
-
1207
- // src/tui/commands/new.ts
1208
- function handleNewCommand(ctx) {
1368
+
1369
+ // src/tui/commands/new.ts
1370
+ function handleNewCommand(ctx) {
1371
+ const { state } = ctx;
1372
+ state.pendingNewThread = true;
1373
+ state.chatContainer.clear();
1374
+ state.pendingTools.clear();
1375
+ state.allToolComponents = [];
1376
+ state.harness.getDisplayState().modifiedFiles.clear();
1377
+ if (state.taskProgress) {
1378
+ state.taskProgress.updateTasks([]);
1379
+ }
1380
+ state.taskWriteInsertIndex = -1;
1381
+ ctx.updateStatusLine();
1382
+ state.ui.requestRender();
1383
+ ctx.showInfo("Ready for new conversation");
1384
+ }
1385
+ function confirmClone(state, threadLabel) {
1386
+ const label = threadLabel ? `Clone thread "${threadLabel}"?` : "Clone the current thread?";
1387
+ return new Promise((resolve2) => {
1388
+ const question = new AskQuestionInlineComponent(
1389
+ {
1390
+ question: label,
1391
+ options: [
1392
+ { label: "Yes", description: "Clone this thread" },
1393
+ { label: "No", description: "Cancel" }
1394
+ ],
1395
+ formatResult: (answer) => answer === "Yes" ? "Cloning thread..." : "Cancelled.",
1396
+ isNegativeAnswer: (answer) => answer !== "Yes",
1397
+ onSubmit: (answer) => {
1398
+ state.activeInlineQuestion = void 0;
1399
+ resolve2(answer === "Yes");
1400
+ },
1401
+ onCancel: () => {
1402
+ state.activeInlineQuestion = void 0;
1403
+ resolve2(false);
1404
+ }
1405
+ },
1406
+ state.ui
1407
+ );
1408
+ state.activeInlineQuestion = question;
1409
+ state.chatContainer.addChild(question);
1410
+ state.chatContainer.addChild(new Spacer(1));
1411
+ state.ui.requestRender();
1412
+ state.chatContainer.invalidate();
1413
+ });
1414
+ }
1415
+ function askCloneName(state) {
1416
+ return new Promise((resolve2) => {
1417
+ const question = new AskQuestionInlineComponent(
1418
+ {
1419
+ question: "Give the cloned thread a name? (Esc to skip)",
1420
+ formatResult: (answer) => `Thread name: ${answer}`,
1421
+ onSubmit: (answer) => {
1422
+ state.activeInlineQuestion = void 0;
1423
+ const trimmed = answer.trim();
1424
+ resolve2(trimmed.length > 0 ? trimmed : null);
1425
+ },
1426
+ onCancel: () => {
1427
+ state.activeInlineQuestion = void 0;
1428
+ resolve2(null);
1429
+ }
1430
+ },
1431
+ state.ui
1432
+ );
1433
+ state.activeInlineQuestion = question;
1434
+ state.chatContainer.addChild(question);
1435
+ state.chatContainer.addChild(new Spacer(1));
1436
+ state.ui.requestRender();
1437
+ state.chatContainer.invalidate();
1438
+ });
1439
+ }
1440
+ async function resetUIAfterClone(ctx, clonedTitle) {
1209
1441
  const { state } = ctx;
1210
- state.pendingNewThread = true;
1211
1442
  state.chatContainer.clear();
1212
1443
  state.pendingTools.clear();
1213
1444
  state.allToolComponents = [];
@@ -1217,8 +1448,28 @@ function handleNewCommand(ctx) {
1217
1448
  }
1218
1449
  state.taskWriteInsertIndex = -1;
1219
1450
  ctx.updateStatusLine();
1451
+ await ctx.renderExistingMessages();
1220
1452
  state.ui.requestRender();
1221
- ctx.showInfo("Ready for new conversation");
1453
+ ctx.showInfo(`Cloned thread: ${clonedTitle}`);
1454
+ }
1455
+ async function handleCloneCommand(ctx) {
1456
+ const { state } = ctx;
1457
+ const currentThreadId = state.harness.getCurrentThreadId();
1458
+ if (!currentThreadId) {
1459
+ ctx.showInfo("No active thread to clone");
1460
+ return;
1461
+ }
1462
+ if (!await confirmClone(state)) return;
1463
+ const customTitle = await askCloneName(state);
1464
+ try {
1465
+ const clonedThread = await state.harness.cloneThread({
1466
+ sourceThreadId: currentThreadId,
1467
+ ...customTitle ? { title: customTitle } : {}
1468
+ });
1469
+ await resetUIAfterClone(ctx, clonedThread.title || clonedThread.id);
1470
+ } catch (error) {
1471
+ ctx.showError(`Failed to clone thread: ${error instanceof Error ? error.message : String(error)}`);
1472
+ }
1222
1473
  }
1223
1474
 
1224
1475
  // src/tui/commands/resource.ts
@@ -1237,21 +1488,42 @@ async function handleResourceCommand(ctx, args) {
1237
1488
  ...knownIds.map((id) => ` ${id === current ? "* " : " "}${id}`),
1238
1489
  "",
1239
1490
  "Usage:",
1240
- " /resource <id> - Switch to a resource ID",
1241
- " /resource reset - Reset to auto-detected ID"
1491
+ " /resource - Show current resource and known IDs",
1492
+ " /resource <id> - Switch to a resource ID (resumes latest thread)",
1493
+ " /resource reset - Reset to auto-detected ID"
1242
1494
  ];
1243
1495
  ctx.showInfo(lines.join("\n"));
1244
1496
  return;
1245
1497
  }
1246
1498
  const newId = sub === "reset" ? defaultId : args.join(" ").trim();
1499
+ if (newId === current) {
1500
+ ctx.showInfo(`Already on resource: ${current}`);
1501
+ return;
1502
+ }
1247
1503
  harness.setResourceId({ resourceId: newId });
1248
- state.pendingNewThread = true;
1249
- state.chatContainer.clear();
1250
- state.pendingTools.clear();
1251
- state.allToolComponents = [];
1504
+ const threads = await harness.listThreads();
1505
+ const latest = [...threads].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())[0];
1506
+ if (latest) {
1507
+ await harness.switchThread({ threadId: latest.id });
1508
+ state.chatContainer.clear();
1509
+ state.pendingTools.clear();
1510
+ state.allToolComponents = [];
1511
+ state.pendingNewThread = false;
1512
+ await ctx.renderExistingMessages();
1513
+ ctx.showInfo(
1514
+ sub === "reset" ? `Resource ID reset to: ${defaultId} \u2014 resumed thread: ${latest.title || latest.id}` : `Switched to resource: ${newId} \u2014 resumed thread: ${latest.title || latest.id}`
1515
+ );
1516
+ } else {
1517
+ state.chatContainer.clear();
1518
+ state.pendingTools.clear();
1519
+ state.allToolComponents = [];
1520
+ state.pendingNewThread = true;
1521
+ ctx.showInfo(
1522
+ sub === "reset" ? `Resource ID reset to: ${defaultId} (no existing threads, a new one will be created)` : `Switched to resource: ${newId} (no existing threads, a new one will be created)`
1523
+ );
1524
+ }
1252
1525
  ctx.updateStatusLine();
1253
1526
  state.ui.requestRender();
1254
- ctx.showInfo(sub === "reset" ? `Resource ID reset to: ${defaultId}` : `Switched to resource: ${newId}`);
1255
1527
  }
1256
1528
  function colorizeDiffLine(line) {
1257
1529
  const t = theme.getTheme();
@@ -1382,8 +1654,10 @@ var ThreadSelectorComponent = class extends Box {
1382
1654
  selectedIndex = 0;
1383
1655
  currentThreadId;
1384
1656
  currentResourceId;
1657
+ currentProjectPath;
1385
1658
  onSelectCallback;
1386
1659
  onCancelCallback;
1660
+ onCloneCallback;
1387
1661
  tui;
1388
1662
  getMessagePreview;
1389
1663
  messagePreviews = /* @__PURE__ */ new Map();
@@ -1400,10 +1674,12 @@ var ThreadSelectorComponent = class extends Box {
1400
1674
  super(2, 1, (text) => theme.bg("overlayBg", text));
1401
1675
  this.tui = options.tui;
1402
1676
  this.currentResourceId = options.currentResourceId;
1677
+ this.currentProjectPath = options.currentProjectPath;
1403
1678
  this.allThreads = this.sortThreads(options.threads, options.currentThreadId);
1404
1679
  this.currentThreadId = options.currentThreadId;
1405
1680
  this.onSelectCallback = options.onSelect;
1406
1681
  this.onCancelCallback = options.onCancel;
1682
+ this.onCloneCallback = options.onClone;
1407
1683
  this.getMessagePreview = options.getMessagePreview;
1408
1684
  this.filteredThreads = this.allThreads;
1409
1685
  this.buildUI();
@@ -1426,7 +1702,10 @@ var ThreadSelectorComponent = class extends Box {
1426
1702
  buildUI() {
1427
1703
  this.addChild(new Text(theme.bold(theme.fg("accent", "Select Thread")), 0, 0));
1428
1704
  this.addChild(new Spacer(1));
1429
- this.addChild(new Text(theme.fg("muted", "Type to search \u2022 \u2191\u2193 navigate \u2022 Enter select \u2022 Esc cancel"), 0, 0));
1705
+ const cloneHint = this.onCloneCallback ? " \u2022 c clone" : "";
1706
+ this.addChild(
1707
+ new Text(theme.fg("muted", `Type to search \u2022 \u2191\u2193 navigate \u2022 Enter select${cloneHint} \u2022 Esc cancel`), 0, 0)
1708
+ );
1430
1709
  this.addChild(new Spacer(1));
1431
1710
  this.searchInput = new Input();
1432
1711
  this.searchInput.onSubmit = () => {
@@ -1444,6 +1723,7 @@ var ThreadSelectorComponent = class extends Box {
1444
1723
  sortThreads(threads, currentThreadId) {
1445
1724
  const sorted = [...threads];
1446
1725
  const resId = this.currentResourceId;
1726
+ const projPath = this.currentProjectPath;
1447
1727
  sorted.sort((a, b) => {
1448
1728
  if (a.id === currentThreadId) return -1;
1449
1729
  if (b.id === currentThreadId) return 1;
@@ -1453,6 +1733,12 @@ var ThreadSelectorComponent = class extends Box {
1453
1733
  if (aLocal && !bLocal) return -1;
1454
1734
  if (!aLocal && bLocal) return 1;
1455
1735
  }
1736
+ if (projPath && a.resourceId === b.resourceId) {
1737
+ const aDir = typeof a.metadata?.projectPath === "string" && a.metadata.projectPath === projPath;
1738
+ const bDir = typeof b.metadata?.projectPath === "string" && b.metadata.projectPath === projPath;
1739
+ if (aDir && !bDir) return -1;
1740
+ if (!aDir && bDir) return 1;
1741
+ }
1456
1742
  return b.updatedAt.getTime() - a.updatedAt.getTime();
1457
1743
  });
1458
1744
  return sorted;
@@ -1461,7 +1747,7 @@ var ThreadSelectorComponent = class extends Box {
1461
1747
  this.filteredThreads = query ? fuzzyFilter(
1462
1748
  this.allThreads,
1463
1749
  query,
1464
- (t) => `${t.title ?? ""} ${t.resourceId} ${t.id} ${t.metadata?.projectPath ?? ""}`
1750
+ (t) => `${t.title ?? ""} ${t.resourceId} ${t.id} ${typeof t.metadata?.projectPath === "string" ? t.metadata.projectPath : ""}`
1465
1751
  ) : this.allThreads;
1466
1752
  this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredThreads.length - 1));
1467
1753
  this.updateList();
@@ -1537,6 +1823,11 @@ var ThreadSelectorComponent = class extends Box {
1537
1823
  }
1538
1824
  } else if (kb.matches(keyData, "selectCancel")) {
1539
1825
  this.onCancelCallback();
1826
+ } else if (keyData === "c" && this.onCloneCallback && !this.searchInput.getValue()) {
1827
+ const selected = this.filteredThreads[this.selectedIndex];
1828
+ if (selected) {
1829
+ this.onCloneCallback(selected);
1830
+ }
1540
1831
  } else {
1541
1832
  this.searchInput.handleInput(keyData);
1542
1833
  this.filterThreads(this.searchInput.getValue());
@@ -1553,18 +1844,39 @@ function truncatePreview(text, maxLength = 50) {
1553
1844
  if (text.length <= maxLength) return text;
1554
1845
  return text.slice(0, maxLength - 3) + "...";
1555
1846
  }
1556
- function showThreadLockPrompt(ctx, threadTitle, ownerPid) {
1847
+ function showThreadLockPrompt(ctx, threadTitle, ownerPid, lockedThreadId) {
1557
1848
  const questionComponent = new AskQuestionInlineComponent(
1558
1849
  {
1559
- question: `Thread "${threadTitle}" is locked by pid ${ownerPid}. Create a new thread?`,
1850
+ question: `Thread "${threadTitle}" is locked by pid ${ownerPid}. What would you like to do?`,
1560
1851
  options: [
1561
- { label: "Yes", description: "Start a new thread" },
1562
- { label: "No", description: "Exit" }
1852
+ { label: "Switch thread", description: "Pick a different thread" },
1853
+ { label: "New thread", description: "Start a fresh thread" },
1854
+ ...lockedThreadId ? [{ label: "Clone thread", description: "Fork from this thread" }] : [],
1855
+ { label: "Exit", description: "Exit" }
1563
1856
  ],
1564
- formatResult: (answer) => answer === "Yes" ? "Thread created" : "Exiting.",
1857
+ formatResult: (answer) => {
1858
+ if (answer === "Switch thread") return "Opening thread selector...";
1859
+ if (answer === "Clone thread") return "Cloning thread...";
1860
+ if (answer === "New thread") return "Starting new thread.";
1861
+ return "Exiting.";
1862
+ },
1565
1863
  onSubmit: async (answer) => {
1566
1864
  ctx.state.activeInlineQuestion = void 0;
1567
- if (!answer.toLowerCase().startsWith("y")) {
1865
+ if (answer === "Switch thread") {
1866
+ await handleThreadsCommand(ctx);
1867
+ } else if (answer === "Clone thread" && lockedThreadId) {
1868
+ try {
1869
+ const customTitle = await askCloneName(ctx.state);
1870
+ const clonedThread = await ctx.state.harness.cloneThread({
1871
+ sourceThreadId: lockedThreadId,
1872
+ ...customTitle ? { title: customTitle } : {}
1873
+ });
1874
+ ctx.state.pendingNewThread = false;
1875
+ await resetUIAfterClone(ctx, clonedThread.title || clonedThread.id);
1876
+ } catch (error) {
1877
+ ctx.showError(`Failed to clone thread: ${error instanceof Error ? error.message : String(error)}`);
1878
+ }
1879
+ } else if (answer === "New thread") ; else {
1568
1880
  process.exit(0);
1569
1881
  }
1570
1882
  },
@@ -1596,6 +1908,7 @@ async function handleThreadsCommand(ctx) {
1596
1908
  threads,
1597
1909
  currentThreadId: currentId,
1598
1910
  currentResourceId,
1911
+ currentProjectPath: state.projectInfo.rootPath,
1599
1912
  getMessagePreview: async (threadId) => {
1600
1913
  const firstUserMessage = await state.harness.getFirstUserMessageForThread({ threadId });
1601
1914
  if (firstUserMessage) {
@@ -1617,7 +1930,7 @@ async function handleThreadsCommand(ctx) {
1617
1930
  await state.harness.switchThread({ threadId: thread.id });
1618
1931
  } catch (error) {
1619
1932
  if (error instanceof ThreadLockError) {
1620
- showThreadLockPrompt(ctx, thread.title || thread.id, error.ownerPid);
1933
+ showThreadLockPrompt(ctx, thread.title || thread.id, error.ownerPid, thread.id);
1621
1934
  } else {
1622
1935
  ctx.showError(`Failed to switch thread: ${error instanceof Error ? error.message : String(error)}`);
1623
1936
  }
@@ -1632,6 +1945,25 @@ async function handleThreadsCommand(ctx) {
1632
1945
  ctx.showInfo(`Switched to: ${thread.title || thread.id}`);
1633
1946
  resolve2();
1634
1947
  },
1948
+ onClone: async (thread) => {
1949
+ state.ui.hideOverlay();
1950
+ if (!await confirmClone(state, thread.title || thread.id)) {
1951
+ resolve2();
1952
+ return;
1953
+ }
1954
+ try {
1955
+ const customTitle = await askCloneName(state);
1956
+ const clonedThread = await state.harness.cloneThread({
1957
+ sourceThreadId: thread.id,
1958
+ ...customTitle ? { title: customTitle } : {}
1959
+ });
1960
+ state.pendingNewThread = false;
1961
+ await resetUIAfterClone(ctx, clonedThread.title || clonedThread.id);
1962
+ } catch (error) {
1963
+ ctx.showError(`Failed to clone thread: ${error instanceof Error ? error.message : String(error)}`);
1964
+ }
1965
+ resolve2();
1966
+ },
1635
1967
  onCancel: () => {
1636
1968
  state.ui.hideOverlay();
1637
1969
  resolve2();
@@ -1694,7 +2026,7 @@ async function handleThreadTagDirCommand(ctx) {
1694
2026
  async function sandboxAddPath(ctx, rawPath) {
1695
2027
  const harnessState = ctx.state.harness.getState();
1696
2028
  const currentPaths = harnessState.sandboxAllowedPaths ?? [];
1697
- const resolved = path4__default.resolve(rawPath);
2029
+ const resolved = path5__default.resolve(rawPath);
1698
2030
  if (currentPaths.includes(resolved)) {
1699
2031
  ctx.showInfo(`Path already allowed: ${resolved}`);
1700
2032
  return;
@@ -1711,7 +2043,7 @@ async function sandboxAddPath(ctx, rawPath) {
1711
2043
  ctx.showInfo(`Added to sandbox: ${resolved}`);
1712
2044
  }
1713
2045
  async function sandboxRemovePath(ctx, rawPath, currentPaths) {
1714
- const resolved = path4__default.resolve(rawPath);
2046
+ const resolved = path5__default.resolve(rawPath);
1715
2047
  const match = currentPaths.find((p) => p === resolved || p === rawPath);
1716
2048
  if (!match) {
1717
2049
  ctx.showError(`Path not in allowed list: ${resolved}`);
@@ -1728,7 +2060,7 @@ async function showSandboxAddPrompt(ctx) {
1728
2060
  {
1729
2061
  question: "Enter path to allow",
1730
2062
  formatResult: (answer) => {
1731
- return `Path: ${path4__default.resolve(answer)}`;
2063
+ return `Path: ${path5__default.resolve(answer)}`;
1732
2064
  },
1733
2065
  onSubmit: async (answer) => {
1734
2066
  ctx.state.activeInlineQuestion = void 0;
@@ -1979,149 +2311,6 @@ var ModelSelectorComponent = class extends Box {
1979
2311
  return this.searchInput;
1980
2312
  }
1981
2313
  };
1982
-
1983
- // src/tui/commands/models.ts
1984
- async function showModelListForScope(ctx, scope, modeId, modeName) {
1985
- const availableModels = await ctx.state.harness.listAvailableModels();
1986
- if (availableModels.length === 0) {
1987
- ctx.showInfo("No models available. Check your Mastra configuration.");
1988
- return;
1989
- }
1990
- const currentModelId = ctx.state.harness.getCurrentModelId();
1991
- const scopeLabel = scope === "global" ? `${modeName} \xB7 Global` : `${modeName} \xB7 Thread`;
1992
- return new Promise((resolve2) => {
1993
- const selector = new ModelSelectorComponent({
1994
- tui: ctx.state.ui,
1995
- models: availableModels,
1996
- currentModelId,
1997
- title: `Select model (${scopeLabel})`,
1998
- onSelect: async (model) => {
1999
- ctx.state.ui.hideOverlay();
2000
- try {
2001
- await ctx.state.harness.switchModel({ modelId: model.id, scope, modeId });
2002
- if (scope === "global") {
2003
- const settings = loadSettings();
2004
- settings.models.activeModelPackId = null;
2005
- settings.models.modeDefaults[modeId] = model.id;
2006
- saveSettings(settings);
2007
- }
2008
- ctx.showInfo(`Model set for ${scopeLabel}: ${model.id}`);
2009
- } catch (err) {
2010
- ctx.showError(`Failed to switch model: ${err instanceof Error ? err.message : String(err)}`);
2011
- }
2012
- resolve2();
2013
- },
2014
- onCancel: () => {
2015
- ctx.state.ui.hideOverlay();
2016
- resolve2();
2017
- }
2018
- });
2019
- ctx.state.ui.showOverlay(selector, {
2020
- width: "80%",
2021
- maxHeight: "60%",
2022
- anchor: "center"
2023
- });
2024
- selector.focused = true;
2025
- });
2026
- }
2027
- async function showModelScopeThenList(ctx, modeId, modeName) {
2028
- const scopes = [
2029
- {
2030
- label: "Thread default",
2031
- description: `Default for ${modeName} mode in this thread`,
2032
- scope: "thread"
2033
- },
2034
- {
2035
- label: "Global default",
2036
- description: `Default for ${modeName} mode in all threads`,
2037
- scope: "global"
2038
- }
2039
- ];
2040
- return new Promise((resolve2) => {
2041
- const questionComponent = new AskQuestionInlineComponent(
2042
- {
2043
- question: `Select scope for ${modeName}`,
2044
- options: scopes.map((s) => ({
2045
- label: s.label,
2046
- description: s.description
2047
- })),
2048
- formatResult: (answer) => `${modeName} \xB7 ${answer}`,
2049
- onSubmit: async (answer) => {
2050
- ctx.state.activeInlineQuestion = void 0;
2051
- try {
2052
- const selected = scopes.find((s) => s.label === answer);
2053
- if (selected) {
2054
- await showModelListForScope(ctx, selected.scope, modeId, modeName);
2055
- }
2056
- } catch (err) {
2057
- ctx.showError(`Model selection failed: ${err instanceof Error ? err.message : String(err)}`);
2058
- }
2059
- resolve2();
2060
- },
2061
- onCancel: () => {
2062
- ctx.state.activeInlineQuestion = void 0;
2063
- resolve2();
2064
- }
2065
- },
2066
- ctx.state.ui
2067
- );
2068
- ctx.state.activeInlineQuestion = questionComponent;
2069
- ctx.state.chatContainer.addChild(new Spacer(1));
2070
- ctx.state.chatContainer.addChild(questionComponent);
2071
- ctx.state.chatContainer.addChild(new Spacer(1));
2072
- ctx.state.ui.requestRender();
2073
- ctx.state.chatContainer.invalidate();
2074
- });
2075
- }
2076
- async function handleModelsCommand(ctx) {
2077
- const modes = ctx.state.harness.listModes();
2078
- const currentMode = ctx.state.harness.getCurrentMode();
2079
- const sortedModes = [...modes].sort((a, b) => {
2080
- if (a.id === currentMode?.id) return -1;
2081
- if (b.id === currentMode?.id) return 1;
2082
- return 0;
2083
- });
2084
- const modeOptions = sortedModes.map((mode) => ({
2085
- label: mode.name + (mode.id === currentMode?.id ? " (active)" : ""),
2086
- modeId: mode.id,
2087
- modeName: mode.name
2088
- }));
2089
- return new Promise((resolve2) => {
2090
- const questionComponent = new AskQuestionInlineComponent(
2091
- {
2092
- question: "Select mode",
2093
- options: modeOptions.map((m) => ({ label: m.label })),
2094
- formatResult: (answer) => {
2095
- const mode = modeOptions.find((m) => m.label === answer);
2096
- return `Mode: ${mode?.modeName ?? answer}`;
2097
- },
2098
- onSubmit: async (answer) => {
2099
- ctx.state.activeInlineQuestion = void 0;
2100
- try {
2101
- const selected = modeOptions.find((m) => m.label === answer);
2102
- if (selected?.modeId && selected?.modeName) {
2103
- await showModelScopeThenList(ctx, selected.modeId, selected.modeName);
2104
- }
2105
- } catch (err) {
2106
- ctx.showError(`Model selection failed: ${err instanceof Error ? err.message : String(err)}`);
2107
- }
2108
- resolve2();
2109
- },
2110
- onCancel: () => {
2111
- ctx.state.activeInlineQuestion = void 0;
2112
- resolve2();
2113
- }
2114
- },
2115
- ctx.state.ui
2116
- );
2117
- ctx.state.activeInlineQuestion = questionComponent;
2118
- ctx.state.chatContainer.addChild(new Spacer(1));
2119
- ctx.state.chatContainer.addChild(questionComponent);
2120
- ctx.state.chatContainer.addChild(new Spacer(1));
2121
- ctx.state.ui.requestRender();
2122
- ctx.state.chatContainer.invalidate();
2123
- });
2124
- }
2125
2314
  var GRADIENT_WIDTH = 30;
2126
2315
  var BASE_COLOR = [124, 58, 237];
2127
2316
  function getMinBrightness() {
@@ -2617,14 +2806,14 @@ function updateStatusLine(state) {
2617
2806
  }
2618
2807
 
2619
2808
  // src/tui/commands/models-pack.ts
2620
- async function selectModel(ctx, title, modeColor) {
2809
+ async function selectModel(ctx, title, modeColor, currentModelId) {
2621
2810
  const availableModels = await ctx.state.harness.listAvailableModels();
2622
2811
  if (availableModels.length === 0) return void 0;
2623
2812
  return new Promise((resolve2) => {
2624
2813
  const selector = new ModelSelectorComponent({
2625
2814
  tui: ctx.state.ui,
2626
2815
  models: availableModels,
2627
- currentModelId: void 0,
2816
+ currentModelId,
2628
2817
  title,
2629
2818
  titleColor: modeColor,
2630
2819
  onSelect: (model) => {
@@ -2644,66 +2833,268 @@ async function selectModel(ctx, title, modeColor) {
2644
2833
  selector.focused = true;
2645
2834
  });
2646
2835
  }
2647
- async function runCustomFlow(ctx) {
2836
+ async function askCustomPackName(ctx, defaultName) {
2837
+ return new Promise((resolve2) => {
2838
+ const question = new AskQuestionInlineComponent(
2839
+ {
2840
+ question: "Name this custom pack",
2841
+ formatResult: (answer) => `Custom pack: ${answer}`,
2842
+ onSubmit: (answer) => {
2843
+ ctx.state.activeInlineQuestion = void 0;
2844
+ const trimmed = answer.trim();
2845
+ resolve2(trimmed.length > 0 ? trimmed : null);
2846
+ },
2847
+ onCancel: () => {
2848
+ ctx.state.activeInlineQuestion = void 0;
2849
+ resolve2(null);
2850
+ }
2851
+ },
2852
+ ctx.state.ui
2853
+ );
2854
+ if (defaultName) {
2855
+ question.input?.setValue?.(defaultName);
2856
+ }
2857
+ ctx.state.activeInlineQuestion = question;
2858
+ ctx.state.chatContainer.addChild(new Spacer(1));
2859
+ ctx.state.chatContainer.addChild(question);
2860
+ ctx.state.chatContainer.addChild(new Spacer(1));
2861
+ ctx.state.ui.requestRender();
2862
+ ctx.state.chatContainer.invalidate();
2863
+ });
2864
+ }
2865
+ async function askCustomPackAction(ctx, pack) {
2866
+ const actions = [
2867
+ { id: "activate", label: "Activate", description: "Use this pack as-is" },
2868
+ { id: "edit", label: "Edit", description: "Update this pack" },
2869
+ { id: "delete", label: "Delete", description: "Remove this custom pack" }
2870
+ ];
2871
+ return new Promise((resolve2) => {
2872
+ const container = new Box(1, 1);
2873
+ container.addChild(new Text(theme.bold(theme.fg("accent", `Custom pack: ${pack.name}`)), 0, 0));
2874
+ container.addChild(new Spacer(1));
2875
+ const items = actions.map((action) => ({
2876
+ value: action.id,
2877
+ label: ` ${action.label} ${theme.fg("dim", action.description)}`
2878
+ }));
2879
+ const selectList = new SelectList(items, items.length, getSelectListTheme());
2880
+ const detailText = new Text("", 0, 0);
2881
+ const detailById = {
2882
+ activate: getPackDetail(pack),
2883
+ edit: theme.fg("dim", " Edit one setting at a time (Rename, plan, build, fast)."),
2884
+ delete: theme.fg("error", " Permanently removes this custom pack from settings.")
2885
+ };
2886
+ selectList.onSelectionChange = (item) => {
2887
+ detailText.setText(detailById[item.value] ?? "");
2888
+ ctx.state.ui.requestRender();
2889
+ };
2890
+ selectList.onSelect = (item) => {
2891
+ ctx.state.activeInlineQuestion = void 0;
2892
+ container.clear();
2893
+ container.addChild(
2894
+ new Text(theme.fg("text", `${theme.fg("success", "\u2713")} ${pack.name} \u2192 ${theme.bold(item.value)}`), 0, 0)
2895
+ );
2896
+ ctx.state.ui.requestRender();
2897
+ resolve2(item.value);
2898
+ };
2899
+ selectList.onCancel = () => {
2900
+ ctx.state.activeInlineQuestion = void 0;
2901
+ container.clear();
2902
+ container.addChild(new Text(theme.fg("dim", `${theme.fg("error", "\u2717")} ${pack.name} (cancelled)`), 0, 0));
2903
+ ctx.state.ui.requestRender();
2904
+ resolve2(null);
2905
+ };
2906
+ detailText.setText(detailById["activate"]);
2907
+ container.addChild(selectList);
2908
+ container.addChild(new Spacer(1));
2909
+ container.addChild(detailText);
2910
+ container.addChild(new Spacer(1));
2911
+ container.addChild(new Text(theme.fg("dim", "\u2191\u2193 navigate \xB7 Enter select \xB7 Esc cancel"), 0, 0));
2912
+ const inputShim = { handleInput: (data) => selectList.handleInput(data) };
2913
+ ctx.state.activeInlineQuestion = inputShim;
2914
+ ctx.state.chatContainer.addChild(container);
2915
+ ctx.state.ui.requestRender();
2916
+ ctx.state.chatContainer.invalidate();
2917
+ });
2918
+ }
2919
+ async function askCustomPackEditTarget(ctx, pack) {
2920
+ return new Promise((resolve2) => {
2921
+ const container = new Box(1, 1);
2922
+ container.addChild(new Text(theme.bold(theme.fg("accent", `Edit custom pack: ${pack.name}`)), 0, 0));
2923
+ container.addChild(new Spacer(1));
2924
+ const selectList = new SelectList(
2925
+ [
2926
+ { value: "rename", label: ` Rename \u2192 ${theme.fg("text", pack.name)}` },
2927
+ { value: "plan", label: ` ${chalk10.hex(mastra.blue)("plan")} \u2192 ${theme.fg("text", pack.models.plan)}` },
2928
+ { value: "build", label: ` ${chalk10.hex(mastra.purple)("build")} \u2192 ${theme.fg("text", pack.models.build)}` },
2929
+ { value: "fast", label: ` ${chalk10.hex(mastra.green)("fast")} \u2192 ${theme.fg("text", pack.models.fast)}` },
2930
+ { value: "save", label: ` ${theme.fg("success", "Save")}` }
2931
+ ],
2932
+ 5,
2933
+ getSelectListTheme()
2934
+ );
2935
+ const cleanup = () => {
2936
+ if (ctx.state.chatContainer.children.includes(container)) {
2937
+ ctx.state.chatContainer.removeChild(container);
2938
+ }
2939
+ ctx.state.ui.requestRender();
2940
+ ctx.state.chatContainer.invalidate();
2941
+ };
2942
+ selectList.onSelect = (item) => {
2943
+ ctx.state.activeInlineQuestion = void 0;
2944
+ cleanup();
2945
+ resolve2(item.value);
2946
+ };
2947
+ selectList.onCancel = () => {
2948
+ ctx.state.activeInlineQuestion = void 0;
2949
+ cleanup();
2950
+ resolve2(null);
2951
+ };
2952
+ container.addChild(selectList);
2953
+ container.addChild(new Spacer(1));
2954
+ container.addChild(new Text(theme.fg("dim", "\u2191\u2193 navigate \xB7 Enter select \xB7 Esc cancel"), 0, 0));
2955
+ const inputShim = { handleInput: (data) => selectList.handleInput(data) };
2956
+ ctx.state.activeInlineQuestion = inputShim;
2957
+ ctx.state.chatContainer.addChild(container);
2958
+ ctx.state.ui.requestRender();
2959
+ ctx.state.chatContainer.invalidate();
2960
+ });
2961
+ }
2962
+ async function runCustomFlow(ctx, options) {
2648
2963
  const modes = [
2649
2964
  { id: "plan", label: "plan", color: mastra.blue },
2650
2965
  { id: "build", label: "build", color: mastra.purple },
2651
2966
  { id: "fast", label: "fast", color: mastra.green }
2652
2967
  ];
2653
- const models = { build: "", plan: "", fast: "" };
2968
+ const name = options?.skipNamePrompt ? options?.name : await askCustomPackName(ctx, void 0);
2969
+ if (!name) return null;
2970
+ const existing = options?.models ?? { build: "", plan: "", fast: "" };
2971
+ const models = {
2972
+ build: existing.build ?? "",
2973
+ plan: existing.plan ?? "",
2974
+ fast: existing.fast ?? ""
2975
+ };
2654
2976
  for (const mode of modes) {
2655
- const modelId = await selectModel(ctx, `Select model for ${mode.label} mode`, mode.color);
2977
+ const modelId = await selectModel(
2978
+ ctx,
2979
+ `Select model for ${mode.label} mode`,
2980
+ mode.color,
2981
+ models[mode.id] || void 0
2982
+ );
2656
2983
  if (!modelId) return null;
2657
2984
  models[mode.id] = modelId;
2658
2985
  }
2659
- return {
2660
- id: "custom",
2661
- name: "Custom",
2662
- description: "User-selected models",
2663
- models
2664
- };
2986
+ return {
2987
+ id: `custom:${name}`,
2988
+ name,
2989
+ description: "Saved custom pack",
2990
+ models
2991
+ };
2992
+ }
2993
+ async function runCustomPackEditFlow(ctx, pack) {
2994
+ let workingPack = { ...pack, models: { ...pack.models } };
2995
+ let previousPackId;
2996
+ while (true) {
2997
+ const editTarget = await askCustomPackEditTarget(ctx, workingPack);
2998
+ if (!editTarget) return null;
2999
+ if (editTarget === "save") return { pack: workingPack, previousPackId };
3000
+ if (editTarget === "rename") {
3001
+ const renamed = await askCustomPackName(ctx, workingPack.name);
3002
+ if (!renamed) continue;
3003
+ const renamedPack = {
3004
+ ...workingPack,
3005
+ id: `custom:${renamed}`,
3006
+ name: renamed
3007
+ };
3008
+ if (renamedPack.id !== pack.id && !previousPackId) previousPackId = pack.id;
3009
+ workingPack = renamedPack;
3010
+ continue;
3011
+ }
3012
+ const modeColors = {
3013
+ plan: mastra.blue,
3014
+ build: mastra.purple,
3015
+ fast: mastra.green
3016
+ };
3017
+ const modelId = await selectModel(
3018
+ ctx,
3019
+ `Select model for ${editTarget} mode`,
3020
+ modeColors[editTarget],
3021
+ workingPack.models[editTarget]
3022
+ );
3023
+ if (!modelId) continue;
3024
+ workingPack = {
3025
+ ...workingPack,
3026
+ models: {
3027
+ ...workingPack.models,
3028
+ [editTarget]: modelId
3029
+ }
3030
+ };
3031
+ }
3032
+ }
3033
+ function upsertCustomPackInSettings(settings, pack, modeDefaults, previousPackId, setActive = true) {
3034
+ if (!pack.id.startsWith("custom:")) return;
3035
+ if (previousPackId && previousPackId.startsWith("custom:") && previousPackId !== pack.id) {
3036
+ removeCustomPackFromSettings(settings, previousPackId);
3037
+ }
3038
+ const customName = pack.id.slice("custom:".length);
3039
+ const entry = { name: customName, models: modeDefaults, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
3040
+ const idx = settings.customModelPacks.findIndex((p) => p.name === customName);
3041
+ if (idx >= 0) {
3042
+ settings.customModelPacks[idx] = entry;
3043
+ } else {
3044
+ settings.customModelPacks.push(entry);
3045
+ }
3046
+ if (setActive) {
3047
+ settings.models.activeModelPackId = pack.id;
3048
+ settings.models.modeDefaults = modeDefaults;
3049
+ }
3050
+ }
3051
+ function removeCustomPackFromSettings(settings, packId) {
3052
+ if (!packId.startsWith("custom:")) return;
3053
+ const packName = packId.slice("custom:".length);
3054
+ const removedPack = settings.customModelPacks.find((p) => p.name === packName);
3055
+ settings.customModelPacks = settings.customModelPacks.filter((p) => p.name !== packName);
3056
+ const modeDefaultsMatchRemovedPack = !!removedPack && settings.models.modeDefaults.plan === removedPack.models.plan && settings.models.modeDefaults.build === removedPack.models.build && settings.models.modeDefaults.fast === removedPack.models.fast;
3057
+ if (settings.models.activeModelPackId === packId) {
3058
+ settings.models.activeModelPackId = null;
3059
+ settings.models.modeDefaults = {};
3060
+ } else if (modeDefaultsMatchRemovedPack) {
3061
+ settings.models.modeDefaults = {};
3062
+ }
3063
+ if (settings.onboarding.modePackId === packId) {
3064
+ settings.onboarding.modePackId = null;
3065
+ }
2665
3066
  }
2666
- function applyPack(ctx, pack) {
3067
+ async function applyPack(ctx, pack, previousPackId) {
2667
3068
  const harness = ctx.state.harness;
2668
3069
  const modes = harness.listModes();
2669
3070
  for (const mode of modes) {
2670
3071
  const modelId = pack.models[mode.id];
2671
3072
  if (modelId) {
2672
3073
  mode.defaultModelId = modelId;
2673
- harness.setThreadSetting({ key: `modeModelId_${mode.id}`, value: modelId });
3074
+ await harness.setThreadSetting({ key: `modeModelId_${mode.id}`, value: modelId });
2674
3075
  }
2675
3076
  }
2676
3077
  const currentModeId = harness.getCurrentModeId();
2677
3078
  const currentModeModel = pack.models[currentModeId];
2678
3079
  if (currentModeModel) {
2679
- harness.switchModel({ modelId: currentModeModel });
3080
+ await harness.switchModel({ modelId: currentModeModel });
2680
3081
  }
2681
3082
  const subagentModeMap = { explore: "fast", plan: "plan", execute: "build" };
2682
3083
  for (const [agentType, modeId] of Object.entries(subagentModeMap)) {
2683
3084
  const saModelId = pack.models[modeId];
2684
3085
  if (saModelId) {
2685
- harness.setSubagentModelId({ modelId: saModelId, agentType });
3086
+ await harness.setSubagentModelId({ modelId: saModelId, agentType });
2686
3087
  }
2687
3088
  }
3089
+ await harness.setThreadSetting({ key: THREAD_ACTIVE_MODEL_PACK_ID_KEY, value: pack.id });
2688
3090
  const s = loadSettings();
2689
3091
  const modeDefaults = {};
2690
3092
  for (const mode of modes) {
2691
3093
  const modelId = pack.models[mode.id];
2692
3094
  if (modelId) modeDefaults[mode.id] = modelId;
2693
3095
  }
2694
- if (pack.id === "custom") {
2695
- const idx = s.customModelPacks.findIndex((p) => p.name === "Setup");
2696
- const entry = { name: "Setup", models: modeDefaults, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
2697
- if (idx >= 0) {
2698
- s.customModelPacks[idx] = entry;
2699
- } else {
2700
- s.customModelPacks.push(entry);
2701
- }
2702
- s.models.activeModelPackId = "custom:Setup";
2703
- s.models.modeDefaults = modeDefaults;
2704
- } else if (pack.id.startsWith("custom:")) {
2705
- s.models.activeModelPackId = pack.id;
2706
- s.models.modeDefaults = modeDefaults;
3096
+ if (pack.id.startsWith("custom:")) {
3097
+ upsertCustomPackInSettings(s, pack, modeDefaults, previousPackId);
2707
3098
  } else {
2708
3099
  s.models.activeModelPackId = pack.id;
2709
3100
  s.models.modeDefaults = {};
@@ -2719,7 +3110,7 @@ function applyPack(ctx, pack) {
2719
3110
  }
2720
3111
  function getPackDetail(pack) {
2721
3112
  if (pack.id === "custom") {
2722
- return theme.fg("dim", " You'll pick a model for each mode.");
3113
+ return theme.fg("dim", " Create a named custom pack and pick a model for each mode.");
2723
3114
  }
2724
3115
  return [
2725
3116
  ` ${chalk10.hex(mastra.blue)("plan")} \u2192 ${theme.fg("text", pack.models.plan)}`,
@@ -2727,6 +3118,46 @@ function getPackDetail(pack) {
2727
3118
  ` ${chalk10.hex(mastra.green)("fast")} \u2192 ${theme.fg("text", pack.models.fast)}`
2728
3119
  ].join("\n");
2729
3120
  }
3121
+ async function saveCustomPackEdits(ctx, pack, previousPackId) {
3122
+ const settings = loadSettings();
3123
+ const wasActive = previousPackId ? settings.models.activeModelPackId === previousPackId : settings.models.activeModelPackId === pack.id;
3124
+ const wasOnboarding = previousPackId ? settings.onboarding.modePackId === previousPackId : settings.onboarding.modePackId === pack.id;
3125
+ const modeDefaults = {
3126
+ plan: pack.models.plan,
3127
+ build: pack.models.build,
3128
+ fast: pack.models.fast
3129
+ };
3130
+ upsertCustomPackInSettings(settings, pack, modeDefaults, previousPackId, false);
3131
+ if (wasActive) {
3132
+ settings.models.activeModelPackId = pack.id;
3133
+ }
3134
+ if (wasOnboarding) {
3135
+ settings.onboarding.modePackId = pack.id;
3136
+ }
3137
+ saveSettings(settings);
3138
+ if (previousPackId && previousPackId !== pack.id) {
3139
+ const harness = ctx.state.harness;
3140
+ const threadId = harness.getCurrentThreadId();
3141
+ const thread = threadId ? (await harness.listThreads()).find((t) => t.id === threadId) : void 0;
3142
+ const threadPackId = thread?.metadata?.[THREAD_ACTIVE_MODEL_PACK_ID_KEY] ?? null;
3143
+ if (threadPackId === previousPackId) {
3144
+ await harness.setThreadSetting({ key: THREAD_ACTIVE_MODEL_PACK_ID_KEY, value: pack.id });
3145
+ }
3146
+ }
3147
+ }
3148
+ async function deleteCustomPack(ctx, pack) {
3149
+ if (!pack.id.startsWith("custom:")) return;
3150
+ const harness = ctx.state.harness;
3151
+ const threadId = harness.getCurrentThreadId();
3152
+ const thread = threadId ? (await harness.listThreads()).find((t) => t.id === threadId) : void 0;
3153
+ const threadPackId = thread?.metadata?.[THREAD_ACTIVE_MODEL_PACK_ID_KEY] ?? null;
3154
+ const settings = loadSettings();
3155
+ removeCustomPackFromSettings(settings, pack.id);
3156
+ saveSettings(settings);
3157
+ if (threadPackId === pack.id) {
3158
+ await harness.setThreadSetting({ key: THREAD_ACTIVE_MODEL_PACK_ID_KEY, value: null });
3159
+ }
3160
+ }
2730
3161
  async function handleModelsPackCommand(ctx) {
2731
3162
  const harness = ctx.state.harness;
2732
3163
  const models = await harness.listAvailableModels();
@@ -2743,13 +3174,26 @@ async function handleModelsPackCommand(ctx) {
2743
3174
  google: hasEnv("google") ? "apikey" : false,
2744
3175
  deepseek: hasEnv("deepseek") ? "apikey" : false
2745
3176
  };
3177
+ const seen = new Set(Object.keys(access));
3178
+ for (const m of models) {
3179
+ if (!seen.has(m.provider) && m.hasApiKey) {
3180
+ access[m.provider] = "apikey";
3181
+ seen.add(m.provider);
3182
+ }
3183
+ }
2746
3184
  const settings = loadSettings();
2747
3185
  const packs = getAvailableModePacks(access, settings.customModelPacks);
2748
3186
  if (packs.length === 0) {
2749
- ctx.showInfo("No model packs available. Run /setup to configure.");
3187
+ ctx.showInfo("No model packs available. Configure provider auth first.");
2750
3188
  return;
2751
3189
  }
2752
- const currentPackId = settings.models.activeModelPackId;
3190
+ const threadId = harness.getCurrentThreadId();
3191
+ const thread = threadId ? (await harness.listThreads()).find((t) => t.id === threadId) : void 0;
3192
+ const currentPackId = resolveThreadActiveModelPackId(
3193
+ settings,
3194
+ packs,
3195
+ thread?.metadata
3196
+ );
2753
3197
  const items = packs.map((p) => ({
2754
3198
  value: p.id,
2755
3199
  label: ` ${p.name} ${theme.fg("dim", p.description)}${p.id === currentPackId ? theme.fg("dim", " (current)") : ""}`
@@ -2766,9 +3210,19 @@ async function handleModelsPackCommand(ctx) {
2766
3210
  detailText.setText(getPackDetail(pack));
2767
3211
  ctx.state.ui.requestRender();
2768
3212
  };
3213
+ const collapseResult = (result) => {
3214
+ container.clear();
3215
+ if (result === "cancelled") {
3216
+ container.addChild(new Text(theme.fg("dim", `${theme.fg("error", "\u2717")} Model pack (cancelled)`), 0, 0));
3217
+ } else if (result) {
3218
+ container.addChild(new Text(theme.fg("text", `${theme.fg("success", "\u2713")} ${result}`), 0, 0));
3219
+ }
3220
+ ctx.state.ui.requestRender();
3221
+ };
2769
3222
  selectList.onSelect = async (item) => {
2770
3223
  ctx.state.activeInlineQuestion = void 0;
2771
- const pack = packs.find((p) => p.id === item.value);
3224
+ let pack = packs.find((p) => p.id === item.value);
3225
+ let previousPackId;
2772
3226
  if (!pack) {
2773
3227
  collapseResult("cancelled");
2774
3228
  resolve2();
@@ -2776,19 +3230,44 @@ async function handleModelsPackCommand(ctx) {
2776
3230
  }
2777
3231
  if (pack.id === "custom") {
2778
3232
  collapseResult(null);
2779
- const customPack = await runCustomFlow(ctx);
2780
- if (customPack) {
2781
- applyPack(ctx, customPack);
2782
- collapseResult(`Model pack \u2192 ${theme.bold("Custom")}`);
2783
- ctx.showInfo("Switched to Custom pack");
2784
- } else {
2785
- collapseResult("cancelled");
3233
+ pack = await runCustomFlow(ctx);
3234
+ } else if (pack.id.startsWith("custom:")) {
3235
+ while (true) {
3236
+ const action = await askCustomPackAction(ctx, pack);
3237
+ if (action === null) {
3238
+ collapseResult("cancelled");
3239
+ resolve2();
3240
+ return;
3241
+ }
3242
+ if (action === "delete") {
3243
+ await deleteCustomPack(ctx, pack);
3244
+ collapseResult(`Deleted custom pack \u2192 ${theme.bold(pack.name)}`);
3245
+ ctx.showInfo(`Deleted custom pack: ${pack.name}`);
3246
+ resolve2();
3247
+ return;
3248
+ }
3249
+ if (action === "activate") {
3250
+ break;
3251
+ }
3252
+ const edited = await runCustomPackEditFlow(ctx, pack);
3253
+ if (!edited) {
3254
+ continue;
3255
+ }
3256
+ previousPackId = edited.previousPackId;
3257
+ pack = edited.pack;
3258
+ await saveCustomPackEdits(ctx, pack, previousPackId);
3259
+ previousPackId = void 0;
3260
+ ctx.showInfo(`Updated custom pack: ${pack.name}`);
2786
3261
  }
2787
- } else {
2788
- applyPack(ctx, pack);
2789
- collapseResult(`Model pack \u2192 ${theme.bold(pack.name)}`);
2790
- ctx.showInfo(`Switched to ${pack.name} pack`);
2791
3262
  }
3263
+ if (!pack) {
3264
+ collapseResult("cancelled");
3265
+ resolve2();
3266
+ return;
3267
+ }
3268
+ await applyPack(ctx, pack, previousPackId);
3269
+ collapseResult(`Model pack \u2192 ${theme.bold(pack.name)}`);
3270
+ ctx.showInfo(`Switched to ${pack.name} pack`);
2792
3271
  resolve2();
2793
3272
  };
2794
3273
  selectList.onCancel = () => {
@@ -2810,14 +3289,6 @@ async function handleModelsPackCommand(ctx) {
2810
3289
  updateDetail(packs[initialIdx].id);
2811
3290
  const inputShim = { handleInput: (data) => selectList.handleInput(data) };
2812
3291
  ctx.state.activeInlineQuestion = inputShim;
2813
- const collapseResult = (result) => {
2814
- container.clear();
2815
- if (result === "cancelled") {
2816
- container.addChild(new Text(theme.fg("dim", `${theme.fg("error", "\u2717")} Model pack (cancelled)`), 0, 0));
2817
- } else if (result) {
2818
- container.addChild(new Text(theme.fg("text", `${theme.fg("success", "\u2713")} ${result}`), 0, 0));
2819
- }
2820
- };
2821
3292
  ctx.state.chatContainer.addChild(new Spacer(1));
2822
3293
  ctx.state.chatContainer.addChild(container);
2823
3294
  ctx.state.chatContainer.addChild(new Spacer(1));
@@ -2825,6 +3296,269 @@ async function handleModelsPackCommand(ctx) {
2825
3296
  ctx.state.chatContainer.invalidate();
2826
3297
  });
2827
3298
  }
3299
+ function isValidUrl(value) {
3300
+ try {
3301
+ const parsed = new URL(value);
3302
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
3303
+ } catch {
3304
+ return false;
3305
+ }
3306
+ }
3307
+ function normalizeProvider(input) {
3308
+ return {
3309
+ name: input.name.trim(),
3310
+ url: input.url.trim(),
3311
+ apiKey: input.apiKey?.trim() || void 0,
3312
+ models: [...new Set(input.models.map((model) => model.trim()).filter(Boolean))]
3313
+ };
3314
+ }
3315
+ function upsertCustomProviderInSettings(settings, provider, previousProviderId) {
3316
+ const next = normalizeProvider(provider);
3317
+ const nextProviderId = getCustomProviderId(next.name);
3318
+ const filteredProviders = settings.customProviders.filter((existing) => {
3319
+ const id = getCustomProviderId(existing.name);
3320
+ return id !== nextProviderId && (!previousProviderId || id !== previousProviderId);
3321
+ });
3322
+ settings.customProviders = [...filteredProviders, next];
3323
+ }
3324
+ function removeCustomProviderFromSettings(settings, providerId) {
3325
+ settings.customProviders = settings.customProviders.filter(
3326
+ (provider) => getCustomProviderId(provider.name) !== providerId
3327
+ );
3328
+ }
3329
+ function addModelToCustomProviderInSettings(settings, providerId, modelName) {
3330
+ const trimmed = modelName.trim();
3331
+ if (!trimmed) return false;
3332
+ const provider = settings.customProviders.find((entry) => getCustomProviderId(entry.name) === providerId);
3333
+ if (!provider) return false;
3334
+ provider.models = [.../* @__PURE__ */ new Set([...provider.models, trimmed])];
3335
+ return true;
3336
+ }
3337
+ function removeModelFromCustomProviderInSettings(settings, providerId, modelName) {
3338
+ const provider = settings.customProviders.find((entry) => getCustomProviderId(entry.name) === providerId);
3339
+ if (!provider) return false;
3340
+ const before = provider.models.length;
3341
+ provider.models = provider.models.filter((model) => model !== modelName);
3342
+ return provider.models.length < before;
3343
+ }
3344
+ function askText(ctx, question, defaultValue, allowEmptyInput = false) {
3345
+ return new Promise((resolve2) => {
3346
+ const component = new AskQuestionInlineComponent(
3347
+ {
3348
+ question,
3349
+ allowEmptyInput,
3350
+ onSubmit: (answer) => {
3351
+ ctx.state.activeInlineQuestion = void 0;
3352
+ const trimmed = answer.trim();
3353
+ resolve2(trimmed.length > 0 ? trimmed : null);
3354
+ },
3355
+ onCancel: () => {
3356
+ ctx.state.activeInlineQuestion = void 0;
3357
+ resolve2(null);
3358
+ }
3359
+ },
3360
+ ctx.state.ui
3361
+ );
3362
+ if (defaultValue) {
3363
+ component.input?.setValue?.(defaultValue);
3364
+ }
3365
+ ctx.state.activeInlineQuestion = component;
3366
+ ctx.state.chatContainer.addChild(new Spacer(1));
3367
+ ctx.state.chatContainer.addChild(component);
3368
+ ctx.state.chatContainer.addChild(new Spacer(1));
3369
+ ctx.state.ui.requestRender();
3370
+ ctx.state.chatContainer.invalidate();
3371
+ });
3372
+ }
3373
+ async function askOptionalText(ctx, question, defaultValue) {
3374
+ const answer = await askText(ctx, `${question} (leave blank to skip)`, defaultValue, true);
3375
+ return answer?.trim() || void 0;
3376
+ }
3377
+ function askSelect(ctx, question, options) {
3378
+ return new Promise((resolve2) => {
3379
+ const component = new AskQuestionInlineComponent(
3380
+ {
3381
+ question,
3382
+ options: options.map((option) => ({ label: option.label, description: option.description })),
3383
+ onSubmit: (answer) => {
3384
+ ctx.state.activeInlineQuestion = void 0;
3385
+ const selected = options.find((option) => option.label === answer);
3386
+ resolve2(selected?.value ?? null);
3387
+ },
3388
+ onCancel: () => {
3389
+ ctx.state.activeInlineQuestion = void 0;
3390
+ resolve2(null);
3391
+ }
3392
+ },
3393
+ ctx.state.ui
3394
+ );
3395
+ ctx.state.activeInlineQuestion = component;
3396
+ ctx.state.chatContainer.addChild(new Spacer(1));
3397
+ ctx.state.chatContainer.addChild(component);
3398
+ ctx.state.chatContainer.addChild(new Spacer(1));
3399
+ ctx.state.ui.requestRender();
3400
+ ctx.state.chatContainer.invalidate();
3401
+ });
3402
+ }
3403
+ async function createProviderFlow(ctx) {
3404
+ const settings = loadSettings();
3405
+ const name = await askText(ctx, "Custom provider name");
3406
+ if (!name) return;
3407
+ const providerId = getCustomProviderId(name);
3408
+ if (settings.customProviders.some((provider) => getCustomProviderId(provider.name) === providerId)) {
3409
+ ctx.showError(`Provider already exists: ${name}`);
3410
+ return;
3411
+ }
3412
+ const url = await askText(ctx, "Base URL (OpenAI-compatible endpoint)");
3413
+ if (!url) return;
3414
+ if (!isValidUrl(url)) {
3415
+ ctx.showError("Invalid URL. Use a full http(s) URL.");
3416
+ return;
3417
+ }
3418
+ const apiKey = await askOptionalText(ctx, "API key");
3419
+ upsertCustomProviderInSettings(settings, { name, url, apiKey, models: [] });
3420
+ saveSettings(settings);
3421
+ ctx.showInfo(`Added custom provider: ${name}`);
3422
+ await manageProviderFlow(ctx, providerId);
3423
+ }
3424
+ async function editProviderFlow(ctx, providerId) {
3425
+ const settings = loadSettings();
3426
+ const provider = settings.customProviders.find((entry) => getCustomProviderId(entry.name) === providerId);
3427
+ if (!provider) {
3428
+ ctx.showError("Provider not found.");
3429
+ return;
3430
+ }
3431
+ const name = await askText(ctx, "Provider name", provider.name);
3432
+ if (!name) return;
3433
+ const nextProviderId = getCustomProviderId(name);
3434
+ if (nextProviderId !== providerId && settings.customProviders.some((entry) => getCustomProviderId(entry.name) === nextProviderId)) {
3435
+ ctx.showError(`Provider already exists: ${name}`);
3436
+ return;
3437
+ }
3438
+ const url = await askText(ctx, "Base URL", provider.url);
3439
+ if (!url) return;
3440
+ if (!isValidUrl(url)) {
3441
+ ctx.showError("Invalid URL. Use a full http(s) URL.");
3442
+ return;
3443
+ }
3444
+ const apiKey = await askOptionalText(ctx, "API key", provider.apiKey);
3445
+ upsertCustomProviderInSettings(
3446
+ settings,
3447
+ {
3448
+ ...provider,
3449
+ name,
3450
+ url,
3451
+ apiKey
3452
+ },
3453
+ providerId
3454
+ );
3455
+ saveSettings(settings);
3456
+ ctx.showInfo(`Updated custom provider: ${name}`);
3457
+ }
3458
+ async function addProviderModelFlow(ctx, providerId) {
3459
+ const settings = loadSettings();
3460
+ const provider = settings.customProviders.find((entry) => getCustomProviderId(entry.name) === providerId);
3461
+ if (!provider) {
3462
+ ctx.showError("Provider not found.");
3463
+ return;
3464
+ }
3465
+ const modelName = await askText(ctx, `Model ID for ${provider.name}`);
3466
+ if (!modelName) return;
3467
+ const added = addModelToCustomProviderInSettings(settings, providerId, modelName);
3468
+ if (!added) {
3469
+ ctx.showError("Unable to add model to provider.");
3470
+ return;
3471
+ }
3472
+ saveSettings(settings);
3473
+ ctx.showInfo(`Added model: ${toCustomProviderModelId(provider.name, modelName)}`);
3474
+ }
3475
+ async function removeProviderModelFlow(ctx, providerId) {
3476
+ const settings = loadSettings();
3477
+ const provider = settings.customProviders.find((entry) => getCustomProviderId(entry.name) === providerId);
3478
+ if (!provider) {
3479
+ ctx.showError("Provider not found.");
3480
+ return;
3481
+ }
3482
+ if (provider.models.length === 0) {
3483
+ ctx.showInfo(`No custom models configured for ${provider.name}.`);
3484
+ return;
3485
+ }
3486
+ const modelName = await askSelect(
3487
+ ctx,
3488
+ `Remove model from ${provider.name}`,
3489
+ provider.models.map((model) => ({
3490
+ label: model,
3491
+ value: model,
3492
+ description: toCustomProviderModelId(provider.name, model)
3493
+ }))
3494
+ );
3495
+ if (!modelName) return;
3496
+ const removed = removeModelFromCustomProviderInSettings(settings, providerId, modelName);
3497
+ if (!removed) {
3498
+ ctx.showError("Unable to remove model from provider.");
3499
+ return;
3500
+ }
3501
+ saveSettings(settings);
3502
+ ctx.showInfo(`Removed model: ${toCustomProviderModelId(provider.name, modelName)}`);
3503
+ }
3504
+ async function manageProviderFlow(ctx, providerId) {
3505
+ const settings = loadSettings();
3506
+ const provider = settings.customProviders.find((entry) => getCustomProviderId(entry.name) === providerId);
3507
+ if (!provider) {
3508
+ ctx.showError("Provider not found.");
3509
+ return;
3510
+ }
3511
+ const action = await askSelect(ctx, `Manage provider: ${provider.name}`, [
3512
+ { label: "Add model", value: "add-model", description: "Attach a model ID to this provider" },
3513
+ { label: "Remove model", value: "remove-model", description: "Remove a model ID from this provider" },
3514
+ { label: "Edit provider", value: "edit-provider", description: "Rename, change URL, or update API key" },
3515
+ { label: "Delete provider", value: "delete-provider", description: "Remove provider and all its model IDs" }
3516
+ ]);
3517
+ switch (action) {
3518
+ case "add-model":
3519
+ await addProviderModelFlow(ctx, providerId);
3520
+ break;
3521
+ case "remove-model":
3522
+ await removeProviderModelFlow(ctx, providerId);
3523
+ break;
3524
+ case "edit-provider":
3525
+ await editProviderFlow(ctx, providerId);
3526
+ break;
3527
+ case "delete-provider": {
3528
+ const confirm = await askSelect(ctx, `Delete ${provider.name}?`, [
3529
+ { label: "Delete", value: "delete", description: "This cannot be undone" }
3530
+ ]);
3531
+ if (confirm !== "delete") return;
3532
+ const latest = loadSettings();
3533
+ removeCustomProviderFromSettings(latest, providerId);
3534
+ saveSettings(latest);
3535
+ ctx.showInfo(`Deleted custom provider: ${provider.name}`);
3536
+ break;
3537
+ }
3538
+ }
3539
+ }
3540
+ async function handleCustomProvidersCommand(ctx) {
3541
+ const settings = loadSettings();
3542
+ const providerOptions = settings.customProviders.map((provider) => {
3543
+ const providerId = getCustomProviderId(provider.name);
3544
+ const modelCount = provider.models.length;
3545
+ return {
3546
+ label: provider.name,
3547
+ value: providerId,
3548
+ description: `${provider.url} \xB7 ${modelCount} model${modelCount === 1 ? "" : "s"} \xB7 ${provider.apiKey ? "api key set" : "no api key"}`
3549
+ };
3550
+ });
3551
+ const action = await askSelect(ctx, "Custom providers", [
3552
+ { label: "Add provider", value: "add-provider", description: "Create an OpenAI-compatible provider" },
3553
+ ...providerOptions
3554
+ ]);
3555
+ if (!action) return;
3556
+ if (action === "add-provider") {
3557
+ await createProviderFlow(ctx);
3558
+ return;
3559
+ }
3560
+ await manageProviderFlow(ctx, action);
3561
+ }
2828
3562
  async function showSubagentModelListForScope(ctx, scope, agentType, agentTypeLabel) {
2829
3563
  const availableModels = await ctx.state.harness.listAvailableModels();
2830
3564
  if (availableModels.length === 0) {
@@ -3239,6 +3973,12 @@ function getShortModelName(modelId) {
3239
3973
  }
3240
3974
 
3241
3975
  // src/tui/commands/om.ts
3976
+ function persistOmModelOverride(modelId) {
3977
+ const settings = loadSettings();
3978
+ settings.models.activeOmPackId = "custom";
3979
+ settings.models.omModelOverride = modelId;
3980
+ saveSettings(settings);
3981
+ }
3242
3982
  async function handleOMCommand(ctx) {
3243
3983
  const availableModels = await ctx.state.harness.listAvailableModels();
3244
3984
  const modelOptions = availableModels.map((m) => ({
@@ -3257,10 +3997,12 @@ async function handleOMCommand(ctx) {
3257
3997
  {
3258
3998
  onObserverModelChange: async (modelId) => {
3259
3999
  await ctx.state.harness.switchObserverModel({ modelId });
4000
+ persistOmModelOverride(modelId);
3260
4001
  ctx.showInfo(`Observer model \u2192 ${modelId}`);
3261
4002
  },
3262
4003
  onReflectorModelChange: async (modelId) => {
3263
4004
  await ctx.state.harness.switchReflectorModel({ modelId });
4005
+ persistOmModelOverride(modelId);
3264
4006
  ctx.showInfo(`Reflector model \u2192 ${modelId}`);
3265
4007
  },
3266
4008
  onObservationThresholdChange: (value) => {
@@ -3485,7 +4227,7 @@ var SettingsComponent = class extends Box {
3485
4227
  {
3486
4228
  id: "escapeAsCancel",
3487
4229
  label: "Escape cancels",
3488
- description: "Use Escape to cancel/clear (Ctrl+C always works). Ctrl+Z undoes a clear.",
4230
+ description: "Use Escape to cancel/clear (Ctrl+C always works).",
3489
4231
  currentValue: config.escapeAsCancel ? "On" : "Off",
3490
4232
  submenu: (_currentValue, done) => new SelectSubmenu(
3491
4233
  [
@@ -3509,6 +4251,33 @@ var SettingsComponent = class extends Box {
3509
4251
  () => done()
3510
4252
  )
3511
4253
  },
4254
+ {
4255
+ id: "quietMode",
4256
+ label: "Quiet mode",
4257
+ description: "Collapse subagent output to a single line after completion.",
4258
+ currentValue: config.quietMode ? "On" : "Off",
4259
+ submenu: (_currentValue, done) => new SelectSubmenu(
4260
+ [
4261
+ {
4262
+ value: "on",
4263
+ label: " On",
4264
+ description: "Auto-collapse subagent output when done"
4265
+ },
4266
+ {
4267
+ value: "off",
4268
+ label: " Off",
4269
+ description: "Keep subagent output visible when done"
4270
+ }
4271
+ ],
4272
+ config.quietMode ? "on" : "off",
4273
+ (value) => {
4274
+ config.quietMode = value === "on";
4275
+ callbacks.onQuietModeChange(config.quietMode);
4276
+ done(config.quietMode ? "On" : "Off");
4277
+ },
4278
+ () => done()
4279
+ )
4280
+ },
3512
4281
  {
3513
4282
  id: "storageBackend",
3514
4283
  label: "Storage backend",
@@ -3557,6 +4326,7 @@ async function handleSettingsCommand(ctx) {
3557
4326
  thinkingLevel: state?.thinkingLevel ?? "off",
3558
4327
  currentModelId: ctx.state.harness.getCurrentModelId() ?? "",
3559
4328
  escapeAsCancel: ctx.state.editor.escapeEnabled,
4329
+ quietMode: globalSettings.preferences.quietMode,
3560
4330
  storageBackend: globalSettings.storage.backend,
3561
4331
  pgConnectionString: globalSettings.storage.pg?.connectionString ?? "",
3562
4332
  libsqlUrl: globalSettings.storage.libsql?.url ?? ""
@@ -3578,6 +4348,12 @@ async function handleSettingsCommand(ctx) {
3578
4348
  await ctx.state.harness.setState({ escapeAsCancel: enabled });
3579
4349
  await ctx.state.harness.setThreadSetting({ key: "escapeAsCancel", value: enabled });
3580
4350
  },
4351
+ onQuietModeChange: (enabled) => {
4352
+ const current = loadSettings();
4353
+ current.preferences.quietMode = enabled;
4354
+ saveSettings(current);
4355
+ ctx.state.quietMode = enabled;
4356
+ },
3581
4357
  onStorageBackendChange: (backend, connectionUrl) => {
3582
4358
  const current = loadSettings();
3583
4359
  current.storage.backend = backend;
@@ -3887,6 +4663,93 @@ Pay special attention to: ${focusArea}
3887
4663
  });
3888
4664
  }
3889
4665
 
4666
+ // src/tui/commands/report-issue.ts
4667
+ var MASTRA_REPO = "mastra-ai/mastra";
4668
+ var MASTRA_LABEL = "mastracode";
4669
+ async function handleReportIssueCommand(ctx, args) {
4670
+ if (!ctx.state.harness.hasModelSelected()) {
4671
+ ctx.showInfo("No model selected. Use /models to select a model, or /login to authenticate.");
4672
+ return;
4673
+ }
4674
+ if (ctx.state.pendingNewThread) {
4675
+ await ctx.state.harness.createThread();
4676
+ ctx.state.pendingNewThread = false;
4677
+ }
4678
+ const extraContext = args.join(" ").trim();
4679
+ const prompt = `The user wants to report a GitHub issue on ${MASTRA_REPO}. Help them through this process.
4680
+
4681
+ ` + (extraContext ? `The user provided this initial context: "${extraContext}"
4682
+
4683
+ ` : "") + `## Step 1: Understand the problem
4684
+
4685
+ Ask the user to describe the issue in their own words. Ask follow-up questions to gather:
4686
+ - What happened / what's wrong
4687
+ - What they expected to happen
4688
+ - Steps to reproduce (if applicable)
4689
+
4690
+ Also gather environment info by running:
4691
+ \`\`\`
4692
+ mastracode --version 2>/dev/null || echo "unknown"
4693
+ node --version
4694
+ uname -s
4695
+ \`\`\`
4696
+
4697
+ Use the conversation history for additional context about what the user was working on when they hit this issue.
4698
+
4699
+ ## Step 2: Check for duplicates
4700
+
4701
+ Once you understand the problem, search for similar existing issues:
4702
+ \`\`\`
4703
+ gh issue list --repo ${MASTRA_REPO} --label ${MASTRA_LABEL} --state open --limit 50 --json number,title,body
4704
+ \`\`\`
4705
+
4706
+ Also search more broadly:
4707
+ \`\`\`
4708
+ gh search issues --repo ${MASTRA_REPO} --state open "<relevant keywords>" --limit 20 --json number,title,body,labels
4709
+ \`\`\`
4710
+
4711
+ If you find similar issue(s):
4712
+ - Present them with their number, title, and a brief summary
4713
+ - Ask the user whether they'd like to add a comment on an existing issue instead of opening a new one
4714
+ - If they choose to comment, draft the comment, show it to the user for approval, then run:
4715
+ \`\`\`
4716
+ gh issue comment <number> --repo ${MASTRA_REPO} --body "<comment>"
4717
+ \`\`\`
4718
+ Then stop here.
4719
+
4720
+ ## Step 3: Draft the issue
4721
+
4722
+ Based on what you've gathered, write a clear, well-structured issue with:
4723
+ - A concise, descriptive title
4724
+ - A body covering: description, expected behavior, steps to reproduce, and environment info
4725
+
4726
+ **Show the full title and body to the user and ask for their approval before creating it.** Let them suggest edits.
4727
+
4728
+ ## Step 4: Create the issue
4729
+
4730
+ Only after the user approves, create the issue:
4731
+ \`\`\`
4732
+ gh issue create --repo ${MASTRA_REPO} --label ${MASTRA_LABEL} --title "<title>" --body "<body>"
4733
+ \`\`\`
4734
+
4735
+ Report the created issue URL back to the user.`;
4736
+ ctx.addUserMessage({
4737
+ id: `user-${Date.now()}`,
4738
+ role: "user",
4739
+ content: [
4740
+ {
4741
+ type: "text",
4742
+ text: extraContext ? `/report-issue ${extraContext}` : "/report-issue"
4743
+ }
4744
+ ],
4745
+ createdAt: /* @__PURE__ */ new Date()
4746
+ });
4747
+ ctx.state.ui.requestRender();
4748
+ ctx.state.harness.sendMessage({ content: prompt }).catch((error) => {
4749
+ ctx.showError(error instanceof Error ? error.message : "Report issue command failed");
4750
+ });
4751
+ }
4752
+
3890
4753
  // src/tui/commands/setup.ts
3891
4754
  async function handleSetupCommand(ctx) {
3892
4755
  await ctx.showOnboarding();
@@ -4310,6 +5173,9 @@ async function dispatchSlashCommand(input, state, buildCtx) {
4310
5173
  case "new":
4311
5174
  handleNewCommand(buildCtx());
4312
5175
  return true;
5176
+ case "clone":
5177
+ await handleCloneCommand(buildCtx());
5178
+ return true;
4313
5179
  case "threads":
4314
5180
  await handleThreadsCommand(buildCtx());
4315
5181
  return true;
@@ -4326,11 +5192,11 @@ async function dispatchSlashCommand(input, state, buildCtx) {
4326
5192
  await handleModeCommand(buildCtx(), args);
4327
5193
  return true;
4328
5194
  case "models":
4329
- await handleModelsCommand(buildCtx());
4330
- return true;
4331
- case "models:pack":
4332
5195
  await handleModelsPackCommand(buildCtx());
4333
5196
  return true;
5197
+ case "custom-providers":
5198
+ await handleCustomProvidersCommand(buildCtx());
5199
+ return true;
4334
5200
  case "subagents":
4335
5201
  await handleSubagentsCommand(buildCtx());
4336
5202
  return true;
@@ -4382,6 +5248,9 @@ async function dispatchSlashCommand(input, state, buildCtx) {
4382
5248
  case "review":
4383
5249
  await handleReviewCommand(buildCtx(), args);
4384
5250
  return true;
5251
+ case "report-issue":
5252
+ await handleReportIssueCommand(buildCtx(), args);
5253
+ return true;
4385
5254
  case "setup":
4386
5255
  await handleSetupCommand(buildCtx());
4387
5256
  return true;
@@ -4490,13 +5359,13 @@ function handleAgentError(ctx) {
4490
5359
  }
4491
5360
  var _compId = 0;
4492
5361
  function asmDebugLog(...args) {
4493
- if (process.env.DEBUG_MASTRA_CODE !== `true`) {
5362
+ if (!["true", "1"].includes(process.env.MASTRA_TUI_DEBUG)) {
4494
5363
  return;
4495
5364
  }
4496
5365
  const line = `[ASM ${(/* @__PURE__ */ new Date()).toISOString()}] ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}
4497
5366
  `;
4498
5367
  try {
4499
- fs2.appendFileSync(path4__default.join(process.cwd(), "tui-debug.log"), line);
5368
+ fs2.appendFileSync(path5__default.join(process.cwd(), "tui-debug.log"), line);
4500
5369
  } catch {
4501
5370
  }
4502
5371
  }
@@ -4918,10 +5787,10 @@ var ToolValidationErrorComponent = class extends Container {
4918
5787
  if (toolName === "ask_user" && errors.some((e) => e.field === "question")) {
4919
5788
  suggestions.push('Make sure to provide a "question" parameter with your question text');
4920
5789
  }
4921
- if (toolName === "execute_command" && errors.some((e) => e.field === "command")) {
5790
+ if (toolName === MC_TOOLS.EXECUTE_COMMAND && errors.some((e) => e.field === "command")) {
4922
5791
  suggestions.push('Provide a "command" parameter with the command to execute');
4923
5792
  }
4924
- if (toolName === "view" && errors.some((e) => e.field === "path")) {
5793
+ if (toolName === MC_TOOLS.VIEW && errors.some((e) => e.field === "path")) {
4925
5794
  suggestions.push('Provide a "path" parameter with the file or directory path');
4926
5795
  }
4927
5796
  return suggestions;
@@ -4929,12 +5798,12 @@ var ToolValidationErrorComponent = class extends Container {
4929
5798
  };
4930
5799
 
4931
5800
  // src/tui/components/tool-execution-enhanced.ts
4932
- function shortenPath(path5) {
5801
+ function shortenPath(path6) {
4933
5802
  const home = os.homedir();
4934
- if (path5.startsWith(home)) {
4935
- return `~${path5.slice(home.length)}`;
5803
+ if (path6.startsWith(home)) {
5804
+ return `~${path6.slice(home.length)}`;
4936
5805
  }
4937
- return path5;
5806
+ return path6;
4938
5807
  }
4939
5808
  function resolveAbsolutePath(filePath) {
4940
5809
  if (filePath.startsWith("/")) return filePath;
@@ -5018,7 +5887,7 @@ var ToolExecutionComponentEnhanced = class extends Container {
5018
5887
  * Only for execute_command tool - shows live output while command runs.
5019
5888
  */
5020
5889
  appendStreamingOutput(output) {
5021
- if (this.toolName !== "execute_command" && this.toolName !== "mastra_workspace_execute_command") {
5890
+ if (this.toolName !== MC_TOOLS.EXECUTE_COMMAND && this.toolName !== MC_TOOLS.GET_PROCESS_OUTPUT && this.toolName !== MC_TOOLS.KILL_PROCESS) {
5022
5891
  return;
5023
5892
  }
5024
5893
  this.streamingOutput += output;
@@ -5042,12 +5911,13 @@ var ToolExecutionComponentEnhanced = class extends Container {
5042
5911
  this.updateBgColor();
5043
5912
  }
5044
5913
  updateBgColor() {
5045
- const isShellCommand = this.toolName === "execute_command" || this.toolName === "mastra_workspace_execute_command";
5046
- const isViewCommand = this.toolName === "view" || this.toolName === "mastra_workspace_read_file";
5047
- const isEditCommand = this.toolName === "string_replace_lsp" || this.toolName === "mastra_workspace_edit_file";
5048
- const isWriteCommand = this.toolName === "write_file" || this.toolName === "mastra_workspace_write_file";
5914
+ const isShellCommand = this.toolName === MC_TOOLS.EXECUTE_COMMAND;
5915
+ const isViewCommand = this.toolName === MC_TOOLS.VIEW;
5916
+ const isEditCommand = this.toolName === MC_TOOLS.STRING_REPLACE_LSP;
5917
+ const isWriteCommand = this.toolName === MC_TOOLS.WRITE_FILE;
5918
+ const isProcessCommand = this.toolName === MC_TOOLS.GET_PROCESS_OUTPUT || this.toolName === MC_TOOLS.KILL_PROCESS;
5049
5919
  const isTaskWrite = this.toolName === "task_write";
5050
- if (isShellCommand || isViewCommand || isEditCommand || isWriteCommand || isTaskWrite) {
5920
+ if (isShellCommand || isViewCommand || isEditCommand || isWriteCommand || isProcessCommand || isTaskWrite) {
5051
5921
  this.contentBox.setBgFn((text) => text);
5052
5922
  return;
5053
5923
  }
@@ -5066,26 +5936,25 @@ var ToolExecutionComponentEnhanced = class extends Container {
5066
5936
  this.contentBox.clear();
5067
5937
  this.collapsible = void 0;
5068
5938
  switch (this.toolName) {
5069
- case "view":
5070
- case "mastra_workspace_read_file":
5939
+ case MC_TOOLS.VIEW:
5071
5940
  this.renderViewToolEnhanced();
5072
5941
  break;
5073
- case "execute_command":
5074
- case "mastra_workspace_execute_command":
5942
+ case MC_TOOLS.EXECUTE_COMMAND:
5075
5943
  this.renderBashToolEnhanced();
5076
5944
  break;
5077
- case "string_replace_lsp":
5078
- case "mastra_workspace_edit_file":
5945
+ case MC_TOOLS.STRING_REPLACE_LSP:
5079
5946
  this.renderEditToolEnhanced();
5080
5947
  break;
5081
- case "write_file":
5082
- case "mastra_workspace_write_file":
5948
+ case MC_TOOLS.WRITE_FILE:
5083
5949
  this.renderWriteToolEnhanced();
5084
5950
  break;
5085
- case "find_files":
5086
- case "mastra_workspace_list_files":
5951
+ case MC_TOOLS.FIND_FILES:
5087
5952
  this.renderListFilesEnhanced();
5088
5953
  break;
5954
+ case MC_TOOLS.GET_PROCESS_OUTPUT:
5955
+ case MC_TOOLS.KILL_PROCESS:
5956
+ this.renderProcessToolEnhanced();
5957
+ break;
5089
5958
  case "task_write":
5090
5959
  this.renderTaskWriteEnhanced();
5091
5960
  break;
@@ -5099,10 +5968,10 @@ var ToolExecutionComponentEnhanced = class extends Container {
5099
5968
  const viewRange = argsObj?.view_range;
5100
5969
  const startLine = viewRange?.[0] ?? argsObj?.offset ?? 1;
5101
5970
  if (!this.result || this.isPartial) {
5102
- const path6 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
5971
+ const path7 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
5103
5972
  const rangeDisplay2 = viewRange ? theme.fg("muted", `:${viewRange[0]},${viewRange[1]}`) : "";
5104
5973
  const status2 = this.getStatusIndicator();
5105
- const pathDisplay2 = fullPath ? fileLink(theme.fg("accent", path6), fullPath, startLine) : theme.fg("accent", path6);
5974
+ const pathDisplay2 = fullPath ? fileLink(theme.fg("accent", path7), fullPath, startLine) : theme.fg("accent", path7);
5106
5975
  const headerText = `${theme.bold(theme.fg("toolTitle", "view"))} ${pathDisplay2}${rangeDisplay2}${status2}`;
5107
5976
  this.contentBox.addChild(new Text(headerText, 0, 0));
5108
5977
  return;
@@ -5113,11 +5982,11 @@ var ToolExecutionComponentEnhanced = class extends Container {
5113
5982
  const termWidth = process.stdout.columns || 80;
5114
5983
  const fixedParts = "\u2514\u2500\u2500 view " + (rangeDisplay ? `:XXX,XXX` : "") + " \u2713";
5115
5984
  const availableForPath = termWidth - fixedParts.length - 6;
5116
- let path5 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
5117
- if (path5.length > availableForPath && availableForPath > 10) {
5118
- path5 = "\u2026" + path5.slice(-(availableForPath - 1));
5985
+ let path6 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
5986
+ if (path6.length > availableForPath && availableForPath > 10) {
5987
+ path6 = "\u2026" + path6.slice(-(availableForPath - 1));
5119
5988
  }
5120
- const pathDisplay = fullPath ? fileLink(theme.fg("accent", path5), fullPath, startLine) : theme.fg("accent", path5);
5989
+ const pathDisplay = fullPath ? fileLink(theme.fg("accent", path6), fullPath, startLine) : theme.fg("accent", path6);
5121
5990
  const footerText = `${theme.bold(theme.fg("toolTitle", "view"))} ${pathDisplay}${rangeDisplay}${status}`;
5122
5991
  this.contentBox.addChild(new Text("", 0, 0));
5123
5992
  this.contentBox.addChild(new Text(border("\u250C\u2500\u2500"), 0, 0));
@@ -5222,9 +6091,50 @@ var ToolExecutionComponentEnhanced = class extends Container {
5222
6091
  renderBorderedShell(status2, prepareOutputLines(output2));
5223
6092
  return;
5224
6093
  }
5225
- const status = theme.fg("success", " \u2713");
6094
+ const status = theme.fg("success", " \u2713");
6095
+ const output = this.streamingOutput.trim() || this.getFormattedOutput();
6096
+ renderBorderedShell(status, prepareOutputLines(output));
6097
+ }
6098
+ renderProcessToolEnhanced() {
6099
+ const argsObj = this.args;
6100
+ const pid = argsObj?.pid ? Number(argsObj.pid) : 0;
6101
+ const isKill = this.toolName === MC_TOOLS.KILL_PROCESS;
6102
+ const isWait = !isKill && argsObj?.wait === true;
6103
+ const timeSuffix = this.isPartial ? "" : this.getDurationSuffix();
6104
+ const label = isKill ? "kill" : isWait ? "wait" : "output";
6105
+ const renderBorderedProcess = (status2, outputLines) => {
6106
+ const border = (char) => theme.bold(theme.fg("accent", char));
6107
+ const footerText = `${theme.fg("toolTitle", label)} ${theme.fg("accent", `PID ${pid}`)}${timeSuffix}${status2}`;
6108
+ this.contentBox.addChild(new Text(border("\u250C\u2500\u2500"), 0, 0));
6109
+ const termWidth = process.stdout.columns || 80;
6110
+ const maxLineWidth = termWidth - 6;
6111
+ const borderedLines = outputLines.map((line) => {
6112
+ const truncated = truncateAnsi(line, maxLineWidth);
6113
+ return border("\u2502") + " " + truncated;
6114
+ });
6115
+ const displayOutput = borderedLines.join("\n");
6116
+ if (displayOutput.trim()) {
6117
+ this.contentBox.addChild(new Text(displayOutput, 0, 0));
6118
+ }
6119
+ this.contentBox.addChild(new Text(`${border("\u2514\u2500\u2500")} ${footerText}`, 0, 0));
6120
+ };
6121
+ const prepareOutputLines = (output2) => {
6122
+ let lines = output2.split("\n");
6123
+ while (lines.length > 0 && lines[0] === "") lines.shift();
6124
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
6125
+ return lines;
6126
+ };
6127
+ if (!this.result || this.isPartial) {
6128
+ const status2 = this.getStatusIndicator();
6129
+ let lines = this.streamingOutput ? this.streamingOutput.split("\n") : [];
6130
+ while (lines.length > 0 && lines[0] === "") lines.shift();
6131
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
6132
+ renderBorderedProcess(status2, lines);
6133
+ return;
6134
+ }
6135
+ const status = this.result.isError ? theme.fg("error", " \u2717") : theme.fg("success", " \u2713");
5226
6136
  const output = this.streamingOutput.trim() || this.getFormattedOutput();
5227
- renderBorderedShell(status, prepareOutputLines(output));
6137
+ renderBorderedProcess(status, prepareOutputLines(output));
5228
6138
  }
5229
6139
  renderEditToolEnhanced() {
5230
6140
  const argsObj = this.args;
@@ -5232,19 +6142,19 @@ var ToolExecutionComponentEnhanced = class extends Container {
5232
6142
  const startLineNum = argsObj?.start_line ? Number(argsObj.start_line) : void 0;
5233
6143
  const startLine = startLineNum ? `:${String(startLineNum)}` : "";
5234
6144
  if (!this.result || this.isPartial) {
5235
- const path6 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
6145
+ const path7 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
5236
6146
  const status2 = this.getStatusIndicator();
5237
- const pathDisplay2 = fullPath ? fileLink(theme.fg("accent", path6), fullPath, startLineNum) : theme.fg("accent", path6);
5238
- if (argsObj?.old_str && argsObj?.new_str) {
6147
+ const pathDisplay2 = fullPath ? fileLink(theme.fg("accent", path7), fullPath, startLineNum) : theme.fg("accent", path7);
6148
+ const oldStr = argsObj?.old_str ?? argsObj?.old_string;
6149
+ const newStr = argsObj?.new_str ?? argsObj?.new_string;
6150
+ if (oldStr != null && newStr != null) {
5239
6151
  const border2 = (char) => theme.bold(theme.fg("accent", char));
5240
6152
  const termWidth2 = process.stdout.columns || 80;
5241
6153
  const maxLineWidth = termWidth2 - 6;
5242
6154
  const footerText2 = `${theme.bold(theme.fg("toolTitle", "edit"))} ${pathDisplay2}${theme.fg("muted", startLine)}${status2}`;
5243
6155
  this.contentBox.addChild(new Text("", 0, 0));
5244
6156
  this.contentBox.addChild(new Text(border2("\u250C\u2500\u2500"), 0, 0));
5245
- const oldStr = String(argsObj.old_str);
5246
- const newStr = String(argsObj.new_str);
5247
- const { lines: diffLines } = this.generateDiffLines(oldStr, newStr);
6157
+ const { lines: diffLines } = this.generateDiffLines(String(oldStr), String(newStr));
5248
6158
  const collapsedLines = 15;
5249
6159
  const totalLines = diffLines.length;
5250
6160
  const hasMore = !this.expanded && totalLines > collapsedLines + 1;
@@ -5276,18 +6186,18 @@ var ToolExecutionComponentEnhanced = class extends Container {
5276
6186
  const termWidth = process.stdout.columns || 80;
5277
6187
  const fixedParts = "\u2514\u2500\u2500 edit " + startLine + " \u2713";
5278
6188
  const availableForPath = termWidth - fixedParts.length - 6;
5279
- let path5 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
5280
- if (path5.length > availableForPath && availableForPath > 10) {
5281
- path5 = "\u2026" + path5.slice(-(availableForPath - 1));
6189
+ let path6 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
6190
+ if (path6.length > availableForPath && availableForPath > 10) {
6191
+ path6 = "\u2026" + path6.slice(-(availableForPath - 1));
5282
6192
  }
5283
- const pathDisplay = fullPath ? fileLink(theme.fg("accent", path5), fullPath, startLineNum) : theme.fg("accent", path5);
6193
+ const pathDisplay = fullPath ? fileLink(theme.fg("accent", path6), fullPath, startLineNum) : theme.fg("accent", path6);
5284
6194
  const footerText = `${theme.bold(theme.fg("toolTitle", "edit"))} ${pathDisplay}${theme.fg("muted", startLine)}${status}`;
5285
6195
  this.contentBox.addChild(new Text("", 0, 0));
5286
6196
  this.contentBox.addChild(new Text(border("\u250C\u2500\u2500"), 0, 0));
5287
- if (argsObj?.old_str && argsObj?.new_str && !this.result.isError) {
5288
- const oldStr = String(argsObj.old_str);
5289
- const newStr = String(argsObj.new_str);
5290
- const { lines: diffLines, firstChangeIndex } = this.generateDiffLines(oldStr, newStr);
6197
+ const finalOldStr = argsObj?.old_str ?? argsObj?.old_string;
6198
+ const finalNewStr = argsObj?.new_str ?? argsObj?.new_string;
6199
+ if (finalOldStr != null && finalNewStr != null && !this.result.isError) {
6200
+ const { lines: diffLines, firstChangeIndex } = this.generateDiffLines(String(finalOldStr), String(finalNewStr));
5291
6201
  const collapsedLines = 15;
5292
6202
  const totalLines = diffLines.length;
5293
6203
  const hasMore = !this.expanded && totalLines > collapsedLines + 1;
@@ -5331,7 +6241,9 @@ var ToolExecutionComponentEnhanced = class extends Container {
5331
6241
  }
5332
6242
  this.contentBox.addChild(new Text(`${border("\u2514\u2500\u2500")} ${footerText}`, 0, 0));
5333
6243
  const diagnostics = this.parseLSPDiagnostics();
5334
- if (diagnostics && diagnostics.hasIssues) {
6244
+ if (diagnostics && !diagnostics.hasIssues) {
6245
+ this.contentBox.addChild(new Text(theme.fg("muted", ` \u2713 No LSP issues`), 0, 0));
6246
+ } else if (diagnostics && diagnostics.hasIssues) {
5335
6247
  const COLLAPSED_DIAG_LINES = 3;
5336
6248
  const shouldCollapse = !this.expanded && diagnostics.entries.length > COLLAPSED_DIAG_LINES + 1;
5337
6249
  const maxDiags = shouldCollapse ? COLLAPSED_DIAG_LINES : diagnostics.entries.length;
@@ -5424,9 +6336,9 @@ var ToolExecutionComponentEnhanced = class extends Container {
5424
6336
  const content = argsObj?.content ? String(argsObj.content) : "";
5425
6337
  if (!this.result || this.isPartial) {
5426
6338
  if (!content) {
5427
- const path7 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
6339
+ const path8 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
5428
6340
  const status3 = this.getStatusIndicator();
5429
- const pathDisplay3 = fullPath ? fileLink(theme.fg("accent", path7), fullPath) : theme.fg("accent", path7);
6341
+ const pathDisplay3 = fullPath ? fileLink(theme.fg("accent", path8), fullPath) : theme.fg("accent", path8);
5430
6342
  const headerText = `${theme.bold(theme.fg("toolTitle", "write"))} ${pathDisplay3}${status3}`;
5431
6343
  this.contentBox.addChild(new Text(headerText, 0, 0));
5432
6344
  return;
@@ -5435,13 +6347,13 @@ var ToolExecutionComponentEnhanced = class extends Container {
5435
6347
  const status2 = this.getStatusIndicator();
5436
6348
  const termWidth2 = process.stdout.columns || 80;
5437
6349
  const maxLineWidth2 = termWidth2 - 6;
5438
- let path6 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
6350
+ let path7 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
5439
6351
  const fixedParts2 = "\u2514\u2500\u2500 write \u22EF";
5440
6352
  const availableForPath2 = termWidth2 - fixedParts2.length - 6;
5441
- if (path6.length > availableForPath2 && availableForPath2 > 10) {
5442
- path6 = "\u2026" + path6.slice(-(availableForPath2 - 1));
6353
+ if (path7.length > availableForPath2 && availableForPath2 > 10) {
6354
+ path7 = "\u2026" + path7.slice(-(availableForPath2 - 1));
5443
6355
  }
5444
- const pathDisplay2 = fullPath ? fileLink(theme.fg("accent", path6), fullPath) : theme.fg("accent", path6);
6356
+ const pathDisplay2 = fullPath ? fileLink(theme.fg("accent", path7), fullPath) : theme.fg("accent", path7);
5445
6357
  const footerText2 = `${theme.bold(theme.fg("toolTitle", "write"))} ${pathDisplay2}${status2}`;
5446
6358
  this.contentBox.addChild(new Text("", 0, 0));
5447
6359
  this.contentBox.addChild(new Text(border2("\u250C\u2500\u2500"), 0, 0));
@@ -5472,13 +6384,13 @@ var ToolExecutionComponentEnhanced = class extends Container {
5472
6384
  const status = this.getStatusIndicator();
5473
6385
  const termWidth = process.stdout.columns || 80;
5474
6386
  const maxLineWidth = termWidth - 6;
5475
- let path5 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
6387
+ let path6 = argsObj?.path ? shortenPath(String(argsObj.path)) : "...";
5476
6388
  const fixedParts = "\u2514\u2500\u2500 write \u2713";
5477
6389
  const availableForPath = termWidth - fixedParts.length - 6;
5478
- if (path5.length > availableForPath && availableForPath > 10) {
5479
- path5 = "\u2026" + path5.slice(-(availableForPath - 1));
6390
+ if (path6.length > availableForPath && availableForPath > 10) {
6391
+ path6 = "\u2026" + path6.slice(-(availableForPath - 1));
5480
6392
  }
5481
- const pathDisplay = fullPath ? fileLink(theme.fg("accent", path5), fullPath) : theme.fg("accent", path5);
6393
+ const pathDisplay = fullPath ? fileLink(theme.fg("accent", path6), fullPath) : theme.fg("accent", path6);
5482
6394
  const footerText = `${theme.bold(theme.fg("toolTitle", "write"))} ${pathDisplay}${status}`;
5483
6395
  this.contentBox.addChild(new Text("", 0, 0));
5484
6396
  this.contentBox.addChild(new Text(border("\u250C\u2500\u2500"), 0, 0));
@@ -5518,25 +6430,26 @@ var ToolExecutionComponentEnhanced = class extends Container {
5518
6430
  renderListFilesEnhanced() {
5519
6431
  const argsObj = this.args;
5520
6432
  const fullPath = argsObj?.path ? String(argsObj.path) : "";
5521
- const path5 = argsObj?.path ? shortenPath(String(argsObj.path)) : "/";
6433
+ const path6 = argsObj?.path ? shortenPath(String(argsObj.path)) : "/";
5522
6434
  const pattern = argsObj?.pattern ? String(argsObj.pattern) : "";
5523
6435
  const patternDisplay = pattern ? " " + theme.fg("muted", pattern) : "";
5524
6436
  if (!this.result || this.isPartial) {
5525
6437
  const status = this.getStatusIndicator();
5526
- const pathDisplay = fullPath ? fileLink(theme.fg("accent", path5), fullPath) : theme.fg("accent", path5);
6438
+ const pathDisplay = fullPath ? fileLink(theme.fg("accent", path6), fullPath) : theme.fg("accent", path6);
5527
6439
  const header = `${theme.bold(theme.fg("toolTitle", "list"))} ${pathDisplay}${patternDisplay}${status}`;
5528
6440
  this.contentBox.addChild(new Text(header, 0, 0));
5529
6441
  return;
5530
6442
  }
5531
6443
  const output = this.getFormattedOutput();
5532
6444
  if (output) {
5533
- const lines = output.split("\n");
5534
- const fileCount = lines.filter((l) => l.trim() && !l.includes("\u2514") && !l.includes("\u251C") && !l.includes("\u2502")).length;
5535
6445
  const listStatus = this.getStatusIndicator();
6446
+ const lines = output.split("\n");
6447
+ const lastLine = lines[lines.length - 1]?.trim() || "";
6448
+ const summaryMatch = lastLine.match(/^\d+\s+directories?,\s+\d+\s+files?$/);
6449
+ const summaryDisplay = summaryMatch ? " " + theme.fg("muted", lastLine) : "";
5536
6450
  this.collapsible = new CollapsibleComponent(
5537
6451
  {
5538
- header: `${theme.bold(theme.fg("toolTitle", "list"))} ${theme.fg("accent", path5)}${patternDisplay}${listStatus}`,
5539
- summary: `${fileCount} items`,
6452
+ header: `${theme.bold(theme.fg("toolTitle", "list"))} ${theme.fg("accent", path6)}${patternDisplay}${summaryDisplay}${listStatus}`,
5540
6453
  expanded: this.expanded,
5541
6454
  collapsedLines: 15,
5542
6455
  expandedLines: 100,
@@ -5715,8 +6628,8 @@ ${stackMatch.join("\n")}`;
5715
6628
  this.contentBox.addChild(errorDisplay);
5716
6629
  }
5717
6630
  };
5718
- function getLanguageFromPath(path5) {
5719
- const ext = path5.split(".").pop()?.toLowerCase();
6631
+ function getLanguageFromPath(path6) {
6632
+ const ext = path6.split(".").pop()?.toLowerCase();
5720
6633
  const langMap = {
5721
6634
  ts: "typescript",
5722
6635
  tsx: "typescript",
@@ -5765,7 +6678,7 @@ function getLanguageFromPath(path5) {
5765
6678
  };
5766
6679
  return ext ? langMap[ext] : void 0;
5767
6680
  }
5768
- function highlightCode(content, path5, startLine) {
6681
+ function highlightCode(content, path6, startLine) {
5769
6682
  let lines = content.split("\n").map((line) => line.trimEnd());
5770
6683
  while (lines.length > 0 && (lines[0].includes("Here's the result of running") || lines[0].match(/^\[Truncated \d+ tokens\]$/) || lines[0].match(/^.*\(\d+ bytes\)$/) || lines[0].match(/^.*\(lines \d+-\d+ of \d+, \d+ bytes\)$/))) {
5771
6684
  lines = lines.slice(1);
@@ -5785,7 +6698,7 @@ function highlightCode(content, path5, startLine) {
5785
6698
  }
5786
6699
  try {
5787
6700
  return highlight(codeLines.join("\n"), {
5788
- language: getLanguageFromPath(path5),
6701
+ language: getLanguageFromPath(path6),
5789
6702
  ignoreIllegals: true
5790
6703
  });
5791
6704
  } catch {
@@ -5921,10 +6834,12 @@ function handleMessageUpdate(ctx, message) {
5921
6834
  }
5922
6835
  }
5923
6836
  const trailingParts = getTrailingContentParts(message);
5924
- state.streamingComponent.updateContent({
5925
- ...message,
5926
- content: trailingParts
5927
- });
6837
+ if (trailingParts.length > 0) {
6838
+ state.streamingComponent.updateContent({
6839
+ ...message,
6840
+ content: trailingParts
6841
+ });
6842
+ }
5928
6843
  state.ui.requestRender();
5929
6844
  }
5930
6845
  function handleMessageEnd(ctx, message) {
@@ -5933,10 +6848,12 @@ function handleMessageEnd(ctx, message) {
5933
6848
  if (state.streamingComponent && message.role === "assistant") {
5934
6849
  state.streamingMessage = message;
5935
6850
  const trailingParts = getTrailingContentParts(message);
5936
- state.streamingComponent.updateContent({
5937
- ...message,
5938
- content: trailingParts
5939
- });
6851
+ if (trailingParts.length > 0 || message.stopReason === "aborted" || message.stopReason === "error") {
6852
+ state.streamingComponent.updateContent({
6853
+ ...message,
6854
+ content: trailingParts
6855
+ });
6856
+ }
5940
6857
  if (message.stopReason === "aborted" || message.stopReason === "error") {
5941
6858
  const errorMessage = message.errorMessage || "Operation aborted";
5942
6859
  for (const [, component] of state.pendingTools) {
@@ -6331,6 +7248,27 @@ function handleOMActivation(ctx, operationType, tokensActivated, observationToke
6331
7248
  state.activeBufferingMarker = void 0;
6332
7249
  state.ui.requestRender();
6333
7250
  }
7251
+ function slugify(str) {
7252
+ const slug = str.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7253
+ return slug || "untitled";
7254
+ }
7255
+ async function savePlanToDisk(opts) {
7256
+ const { title, plan, resourceId } = opts;
7257
+ const plansDir = opts.plansDir ?? process.env.MASTRA_PLANS_DIR ?? path5__default.join(getAppDataDir(), "plans");
7258
+ const dir = path5__default.join(plansDir, resourceId);
7259
+ await fs4.mkdir(dir, { recursive: true });
7260
+ const now = /* @__PURE__ */ new Date();
7261
+ const timestamp = now.toISOString().replace(/:/g, "-");
7262
+ const slug = slugify(title);
7263
+ const filename = `${timestamp}-${slug}.md`;
7264
+ const content = `# ${title}
7265
+
7266
+ Approved: ${now.toISOString()}
7267
+
7268
+ ${plan}
7269
+ `;
7270
+ await fs4.writeFile(path5__default.join(dir, filename), content, "utf-8");
7271
+ }
6334
7272
  var AskQuestionDialogComponent = class extends Box {
6335
7273
  selectList;
6336
7274
  input;
@@ -6488,6 +7426,11 @@ var PlanApprovalInlineComponent = class extends Container {
6488
7426
  this.mode = "feedback";
6489
7427
  this.selectList = void 0;
6490
7428
  this.contentBox.clear();
7429
+ this.contentBox.addChild(new Text(theme.bold(theme.fg("accent", `Plan: ${this.planTitle}`)), 0, 0));
7430
+ this.contentBox.addChild(new Spacer(1));
7431
+ const md = new Markdown(this.planContent, 1, 0, getMarkdownTheme());
7432
+ this.contentBox.addChild(md);
7433
+ this.contentBox.addChild(new Spacer(1));
6491
7434
  this.contentBox.addChild(new Text(theme.fg("accent", "Provide feedback for revision:"), 0, 0));
6492
7435
  this.contentBox.addChild(new Spacer(1));
6493
7436
  this.feedbackInput = new Input();
@@ -6558,55 +7501,71 @@ var PlanResultComponent = class extends Container {
6558
7501
  };
6559
7502
 
6560
7503
  // src/tui/handlers/prompts.ts
7504
+ function processNextInlineQuestion(state) {
7505
+ const next = state.pendingInlineQuestions.shift();
7506
+ if (next) {
7507
+ next();
7508
+ }
7509
+ }
6561
7510
  async function handleAskQuestion(ctx, questionId, question, options) {
6562
7511
  const { state } = ctx;
6563
7512
  return new Promise((resolve2) => {
6564
7513
  if (state.options.inlineQuestions) {
6565
- const questionComponent = new AskQuestionInlineComponent(
6566
- {
6567
- question,
6568
- options,
6569
- onSubmit: (answer) => {
6570
- state.activeInlineQuestion = void 0;
6571
- state.harness.respondToQuestion({ questionId, answer });
6572
- resolve2();
7514
+ const askUserComponent = state.lastAskUserComponent;
7515
+ const activate = () => {
7516
+ const questionComponent = new AskQuestionInlineComponent(
7517
+ {
7518
+ question,
7519
+ options,
7520
+ onSubmit: (answer) => {
7521
+ state.activeInlineQuestion = void 0;
7522
+ state.harness.respondToQuestion({ questionId, answer });
7523
+ resolve2();
7524
+ processNextInlineQuestion(state);
7525
+ },
7526
+ onCancel: () => {
7527
+ state.activeInlineQuestion = void 0;
7528
+ state.harness.respondToQuestion({ questionId, answer: "(skipped)" });
7529
+ resolve2();
7530
+ processNextInlineQuestion(state);
7531
+ }
6573
7532
  },
6574
- onCancel: () => {
6575
- state.activeInlineQuestion = void 0;
6576
- state.harness.respondToQuestion({ questionId, answer: "(skipped)" });
6577
- resolve2();
6578
- }
6579
- },
6580
- state.ui
6581
- );
6582
- state.activeInlineQuestion = questionComponent;
6583
- if (state.lastAskUserComponent) {
6584
- const children = [...state.chatContainer.children];
6585
- const askUserIndex = children.indexOf(state.lastAskUserComponent);
6586
- if (askUserIndex >= 0) {
6587
- state.chatContainer.clear();
6588
- for (let i = 0; i <= askUserIndex; i++) {
6589
- state.chatContainer.addChild(children[i]);
6590
- }
6591
- state.chatContainer.addChild(new Spacer(1));
6592
- state.chatContainer.addChild(questionComponent);
6593
- state.chatContainer.addChild(new Spacer(1));
6594
- for (let i = askUserIndex + 1; i < children.length; i++) {
6595
- state.chatContainer.addChild(children[i]);
7533
+ state.ui
7534
+ );
7535
+ state.activeInlineQuestion = questionComponent;
7536
+ if (askUserComponent) {
7537
+ const children = [...state.chatContainer.children];
7538
+ const askUserIndex = children.indexOf(askUserComponent);
7539
+ if (askUserIndex >= 0) {
7540
+ state.chatContainer.clear();
7541
+ for (let i = 0; i <= askUserIndex; i++) {
7542
+ state.chatContainer.addChild(children[i]);
7543
+ }
7544
+ state.chatContainer.addChild(new Spacer(1));
7545
+ state.chatContainer.addChild(questionComponent);
7546
+ state.chatContainer.addChild(new Spacer(1));
7547
+ for (let i = askUserIndex + 1; i < children.length; i++) {
7548
+ state.chatContainer.addChild(children[i]);
7549
+ }
7550
+ } else {
7551
+ state.chatContainer.addChild(new Spacer(1));
7552
+ state.chatContainer.addChild(questionComponent);
7553
+ state.chatContainer.addChild(new Spacer(1));
6596
7554
  }
6597
7555
  } else {
6598
7556
  state.chatContainer.addChild(new Spacer(1));
6599
7557
  state.chatContainer.addChild(questionComponent);
6600
7558
  state.chatContainer.addChild(new Spacer(1));
6601
7559
  }
7560
+ state.ui.requestRender();
7561
+ state.chatContainer.invalidate();
7562
+ questionComponent.focused = true;
7563
+ };
7564
+ if (state.activeInlineQuestion) {
7565
+ state.pendingInlineQuestions.push(activate);
6602
7566
  } else {
6603
- state.chatContainer.addChild(new Spacer(1));
6604
- state.chatContainer.addChild(questionComponent);
6605
- state.chatContainer.addChild(new Spacer(1));
7567
+ activate();
6606
7568
  }
6607
- state.ui.requestRender();
6608
- state.chatContainer.invalidate();
6609
- questionComponent.focused = true;
6610
7569
  } else {
6611
7570
  const dialog = new AskQuestionDialogComponent({
6612
7571
  question,
@@ -6631,39 +7590,48 @@ async function handleAskQuestion(ctx, questionId, question, options) {
6631
7590
  async function handleSandboxAccessRequest(ctx, questionId, requestedPath, reason) {
6632
7591
  const { state } = ctx;
6633
7592
  return new Promise((resolve2) => {
6634
- const questionComponent = new AskQuestionInlineComponent(
6635
- {
6636
- question: `Grant sandbox access to "${requestedPath}"?
7593
+ const activate = () => {
7594
+ const questionComponent = new AskQuestionInlineComponent(
7595
+ {
7596
+ question: `Grant sandbox access to "${requestedPath}"?
6637
7597
  ${theme.fg("dim", `Reason: ${reason}`)}`,
6638
- options: [
6639
- { label: "Yes", description: "Allow access to this directory" },
6640
- { label: "No", description: "Deny access" }
6641
- ],
6642
- onSubmit: (answer) => {
6643
- state.activeInlineQuestion = void 0;
6644
- state.harness.respondToQuestion({ questionId, answer });
6645
- resolve2();
6646
- },
6647
- onCancel: () => {
6648
- state.activeInlineQuestion = void 0;
6649
- state.harness.respondToQuestion({ questionId, answer: "No" });
6650
- resolve2();
6651
- },
6652
- formatResult: (answer) => {
6653
- const approved = answer.toLowerCase().startsWith("y");
6654
- return approved ? `Granted access to ${requestedPath}` : `Denied access to ${requestedPath}`;
7598
+ options: [
7599
+ { label: "Yes", description: "Allow access to this directory" },
7600
+ { label: "No", description: "Deny access" }
7601
+ ],
7602
+ onSubmit: (answer) => {
7603
+ state.activeInlineQuestion = void 0;
7604
+ state.harness.respondToQuestion({ questionId, answer });
7605
+ resolve2();
7606
+ processNextInlineQuestion(state);
7607
+ },
7608
+ onCancel: () => {
7609
+ state.activeInlineQuestion = void 0;
7610
+ state.harness.respondToQuestion({ questionId, answer: "No" });
7611
+ resolve2();
7612
+ processNextInlineQuestion(state);
7613
+ },
7614
+ formatResult: (answer) => {
7615
+ const approved = answer.toLowerCase().startsWith("y");
7616
+ return approved ? `Granted access to ${requestedPath}` : `Denied access to ${requestedPath}`;
7617
+ },
7618
+ isNegativeAnswer: (answer) => !answer.toLowerCase().startsWith("y")
6655
7619
  },
6656
- isNegativeAnswer: (answer) => !answer.toLowerCase().startsWith("y")
6657
- },
6658
- state.ui
6659
- );
6660
- state.activeInlineQuestion = questionComponent;
6661
- state.chatContainer.addChild(new Spacer(1));
6662
- state.chatContainer.addChild(questionComponent);
6663
- state.chatContainer.addChild(new Spacer(1));
6664
- questionComponent.focused = true;
6665
- state.ui.requestRender();
6666
- state.chatContainer.invalidate();
7620
+ state.ui
7621
+ );
7622
+ state.activeInlineQuestion = questionComponent;
7623
+ state.chatContainer.addChild(new Spacer(1));
7624
+ state.chatContainer.addChild(questionComponent);
7625
+ state.chatContainer.addChild(new Spacer(1));
7626
+ questionComponent.focused = true;
7627
+ state.ui.requestRender();
7628
+ state.chatContainer.invalidate();
7629
+ };
7630
+ if (state.activeInlineQuestion) {
7631
+ state.pendingInlineQuestions.push(activate);
7632
+ } else {
7633
+ activate();
7634
+ }
6667
7635
  ctx.notify("sandbox_access", `Sandbox access requested: ${requestedPath}`);
6668
7636
  });
6669
7637
  }
@@ -6684,6 +7652,12 @@ async function handlePlanApproval(ctx, planId, title, plan) {
6684
7652
  approvedAt: (/* @__PURE__ */ new Date()).toISOString()
6685
7653
  }
6686
7654
  });
7655
+ savePlanToDisk({
7656
+ title,
7657
+ plan,
7658
+ resourceId: state.harness.getResourceId()
7659
+ }).catch(() => {
7660
+ });
6687
7661
  await state.harness.respondToPlanApproval({
6688
7662
  planId,
6689
7663
  response: { action: "approved" }
@@ -6757,12 +7731,14 @@ var SubagentExecutionComponent = class extends Container {
6757
7731
  durationMs = 0;
6758
7732
  finalResult;
6759
7733
  expanded = false;
6760
- constructor(agentType, task, ui, modelId) {
7734
+ collapseOnComplete;
7735
+ constructor(agentType, task, ui, modelId, options) {
6761
7736
  super();
6762
7737
  this.agentType = agentType;
6763
7738
  this.task = task;
6764
7739
  this.modelId = modelId;
6765
7740
  this.ui = ui;
7741
+ this.collapseOnComplete = options?.collapseOnComplete ?? false;
6766
7742
  this.rebuild();
6767
7743
  }
6768
7744
  // ── Mutation API ──────────────────────────────────────────────────────
@@ -6787,6 +7763,9 @@ var SubagentExecutionComponent = class extends Container {
6787
7763
  this.isError = isError;
6788
7764
  this.durationMs = durationMs;
6789
7765
  this.finalResult = result;
7766
+ if (this.collapseOnComplete) {
7767
+ this.expanded = false;
7768
+ }
6790
7769
  this.rebuild();
6791
7770
  }
6792
7771
  setExpanded(expanded) {
@@ -6809,6 +7788,17 @@ var SubagentExecutionComponent = class extends Container {
6809
7788
  const border = (char) => theme.bold(theme.fg("accent", char));
6810
7789
  const termWidth = process.stdout.columns || 80;
6811
7790
  const maxLineWidth = termWidth - 6;
7791
+ const typeLabel = theme.bold(theme.fg("accent", this.agentType));
7792
+ const modelLabel = this.modelId ? theme.fg("muted", ` ${this.modelId}`) : "";
7793
+ const statusIcon = this.done ? this.isError ? theme.fg("error", " \u2717") : theme.fg("success", " \u2713") : theme.fg("muted", " \u22EF");
7794
+ const durationStr = this.done ? theme.fg("muted", ` ${formatDuration(this.durationMs)}`) : "";
7795
+ const footerText = `${theme.bold(theme.fg("toolTitle", "subagent"))} ${typeLabel}${modelLabel}${durationStr}${statusIcon}`;
7796
+ if (this.collapseOnComplete && this.done && !this.expanded) {
7797
+ this.addChild(new Text(`${border("\u2514\u2500\u2500")} ${footerText}`, 0, 0));
7798
+ this.invalidate();
7799
+ this.ui.requestRender();
7800
+ return;
7801
+ }
6812
7802
  this.addChild(new Text(border("\u250C\u2500\u2500"), 0, 0));
6813
7803
  const taskLines = this.task.split("\n");
6814
7804
  const wrappedTaskLines = [];
@@ -6861,18 +7851,10 @@ var SubagentExecutionComponent = class extends Container {
6861
7851
  this.addChild(new Text(`${border("\u2502")} ${moreText}`, 0, 0));
6862
7852
  }
6863
7853
  }
6864
- const showResult = this.done && this.finalResult && (this.expanded || this.toolCalls.length === 0);
6865
- if (showResult) {
7854
+ if (this.done && this.finalResult && this.expanded) {
6866
7855
  this.addChild(new Text(`${border("\u2502")} ${theme.fg("muted", "\u2500\u2500\u2500")}`, 0, 0));
6867
7856
  const resultLines = this.finalResult.split("\n");
6868
- const maxResultLines = this.expanded ? resultLines.length : 10;
6869
- const truncated = !this.expanded && resultLines.length > maxResultLines + 1;
6870
- const displayLines = truncated ? resultLines.slice(-maxResultLines) : resultLines;
6871
- if (truncated) {
6872
- const hiddenLine = `${border("\u2502")} ${theme.fg("muted", ` ... ${resultLines.length - maxResultLines} more lines (ctrl+e to expand)`)}`;
6873
- this.addChild(new Text(hiddenLine, 0, 0));
6874
- }
6875
- const resultContent = displayLines.map((line) => {
7857
+ const resultContent = resultLines.map((line) => {
6876
7858
  const truncatedLine = line.length > maxLineWidth ? line.slice(0, maxLineWidth - 1) + "\u2026" : line;
6877
7859
  return `${border("\u2502")} ${theme.fg("muted", truncatedLine)}`;
6878
7860
  }).join("\n");
@@ -6880,11 +7862,6 @@ var SubagentExecutionComponent = class extends Container {
6880
7862
  this.addChild(new Text(resultContent, 0, 0));
6881
7863
  }
6882
7864
  }
6883
- const typeLabel = theme.bold(theme.fg("accent", this.agentType));
6884
- const modelLabel = this.modelId ? theme.fg("muted", ` ${this.modelId}`) : "";
6885
- const statusIcon = this.done ? this.isError ? theme.fg("error", " \u2717") : theme.fg("success", " \u2713") : theme.fg("muted", " \u22EF");
6886
- const durationStr = this.done ? theme.fg("muted", ` ${formatDuration(this.durationMs)}`) : "";
6887
- const footerText = `${theme.bold(theme.fg("toolTitle", "subagent"))} ${typeLabel}${modelLabel}${durationStr}${statusIcon}`;
6888
7865
  this.addChild(new Text(`${border("\u2514\u2500\u2500")} ${footerText}`, 0, 0));
6889
7866
  this.invalidate();
6890
7867
  this.ui.requestRender();
@@ -6930,7 +7907,9 @@ function summarizeArgs(args) {
6930
7907
  // src/tui/handlers/subagent.ts
6931
7908
  function handleSubagentStart(ctx, toolCallId, agentType, task, modelId) {
6932
7909
  const { state } = ctx;
6933
- const component = new SubagentExecutionComponent(agentType, task, state.ui, modelId);
7910
+ const component = new SubagentExecutionComponent(agentType, task, state.ui, modelId, {
7911
+ collapseOnComplete: state.quietMode
7912
+ });
6934
7913
  state.pendingSubagents.set(toolCallId, component);
6935
7914
  state.allToolComponents.push(component);
6936
7915
  if (state.streamingComponent) {
@@ -7622,7 +8601,8 @@ async function renderExistingMessages(state) {
7622
8601
  subArgs?.agentType ?? "unknown",
7623
8602
  subArgs?.task ?? "",
7624
8603
  state.ui,
7625
- modelId
8604
+ modelId,
8605
+ { collapseOnComplete: state.quietMode }
7626
8606
  );
7627
8607
  if (meta?.toolCalls) {
7628
8608
  for (const tc of meta.toolCalls) {
@@ -7757,7 +8737,7 @@ async function parseCommandFile(filePath, baseDir) {
7757
8737
  const content = await promises.readFile(filePath, "utf-8");
7758
8738
  const trimmedContent = content.trim();
7759
8739
  if (!trimmedContent.startsWith("---")) {
7760
- const name2 = baseDir ? extractCommandName(filePath, baseDir) : path4.basename(filePath, ".md");
8740
+ const name2 = baseDir ? extractCommandName(filePath, baseDir) : path5.basename(filePath, ".md");
7761
8741
  return {
7762
8742
  name: name2,
7763
8743
  description: "",
@@ -7778,7 +8758,7 @@ async function parseCommandFile(filePath, baseDir) {
7778
8758
  } else if (baseDir) {
7779
8759
  name = extractCommandName(filePath, baseDir);
7780
8760
  } else {
7781
- name = path4.basename(filePath, ".md");
8761
+ name = path5.basename(filePath, ".md");
7782
8762
  }
7783
8763
  return {
7784
8764
  name,
@@ -7793,9 +8773,9 @@ async function parseCommandFile(filePath, baseDir) {
7793
8773
  }
7794
8774
  }
7795
8775
  function extractCommandName(filePath, baseDir) {
7796
- const relativePath = path4.relative(baseDir, filePath);
7797
- const dirName = path4.dirname(relativePath);
7798
- const baseName = path4.basename(relativePath, ".md");
8776
+ const relativePath = path5.relative(baseDir, filePath);
8777
+ const dirName = path5.dirname(relativePath);
8778
+ const baseName = path5.basename(relativePath, ".md");
7799
8779
  if (dirName === "." || dirName === "") {
7800
8780
  return baseName;
7801
8781
  }
@@ -7807,7 +8787,7 @@ async function scanCommandDirectory(dirPath) {
7807
8787
  try {
7808
8788
  const entries = await promises.readdir(dirPath, { withFileTypes: true });
7809
8789
  for (const entry of entries) {
7810
- const fullPath = path4.join(dirPath, entry.name);
8790
+ const fullPath = path5.join(dirPath, entry.name);
7811
8791
  if (entry.isDirectory()) {
7812
8792
  const subCommands = await scanCommandDirectory(fullPath);
7813
8793
  commands.push(...subCommands);
@@ -7835,32 +8815,32 @@ async function loadCustomCommands(projectDir) {
7835
8815
  };
7836
8816
  const homeDir = process.env.HOME || process.env.USERPROFILE;
7837
8817
  if (homeDir) {
7838
- const opencodeUserDir = path4.join(homeDir, ".opencode", "command");
8818
+ const opencodeUserDir = path5.join(homeDir, ".opencode", "command");
7839
8819
  const opencodeUserCommands = await scanCommandDirectory(opencodeUserDir);
7840
8820
  addCommands(opencodeUserCommands);
7841
8821
  }
7842
8822
  if (homeDir) {
7843
- const claudeUserDir = path4.join(homeDir, ".claude", "commands");
8823
+ const claudeUserDir = path5.join(homeDir, ".claude", "commands");
7844
8824
  const claudeUserCommands = await scanCommandDirectory(claudeUserDir);
7845
8825
  addCommands(claudeUserCommands);
7846
8826
  }
7847
8827
  if (homeDir) {
7848
- const mastraUserDir = path4.join(homeDir, ".mastracode", "commands");
8828
+ const mastraUserDir = path5.join(homeDir, ".mastracode", "commands");
7849
8829
  const mastraUserCommands = await scanCommandDirectory(mastraUserDir);
7850
8830
  addCommands(mastraUserCommands);
7851
8831
  }
7852
8832
  if (projectDir) {
7853
- const opencodeProjectDir = path4.join(projectDir, ".opencode", "command");
8833
+ const opencodeProjectDir = path5.join(projectDir, ".opencode", "command");
7854
8834
  const opencodeProjectCommands = await scanCommandDirectory(opencodeProjectDir);
7855
8835
  addCommands(opencodeProjectCommands);
7856
8836
  }
7857
8837
  if (projectDir) {
7858
- const claudeProjectDir = path4.join(projectDir, ".claude", "commands");
8838
+ const claudeProjectDir = path5.join(projectDir, ".claude", "commands");
7859
8839
  const claudeProjectCommands = await scanCommandDirectory(claudeProjectDir);
7860
8840
  addCommands(claudeProjectCommands);
7861
8841
  }
7862
8842
  if (projectDir) {
7863
- const mastraProjectDir = path4.join(projectDir, ".mastracode", "commands");
8843
+ const mastraProjectDir = path5.join(projectDir, ".mastracode", "commands");
7864
8844
  const mastraProjectCommands = await scanCommandDirectory(mastraProjectDir);
7865
8845
  addCommands(mastraProjectCommands);
7866
8846
  }
@@ -7975,11 +8955,13 @@ function setupKeyboardShortcuts(state, callbacks) {
7975
8955
  state.pendingApprovalDismiss();
7976
8956
  state.activeInlinePlanApproval = void 0;
7977
8957
  state.activeInlineQuestion = void 0;
8958
+ state.pendingInlineQuestions.length = 0;
7978
8959
  state.userInitiatedAbort = true;
7979
8960
  state.harness.abort();
7980
8961
  } else if (state.harness.isRunning()) {
7981
8962
  state.activeInlinePlanApproval = void 0;
7982
8963
  state.activeInlineQuestion = void 0;
8964
+ state.pendingInlineQuestions.length = 0;
7983
8965
  state.userInitiatedAbort = true;
7984
8966
  state.harness.abort();
7985
8967
  } else {
@@ -7991,6 +8973,26 @@ function setupKeyboardShortcuts(state, callbacks) {
7991
8973
  state.ui.requestRender();
7992
8974
  }
7993
8975
  });
8976
+ state.editor.onAction("suspend", () => {
8977
+ if (process.platform === "win32") {
8978
+ showInfo(state, "Suspend is not supported on Windows");
8979
+ return;
8980
+ }
8981
+ state.ui.stop();
8982
+ const onContinue = () => {
8983
+ state.ui.start();
8984
+ state.ui.requestRender();
8985
+ };
8986
+ process.once("SIGCONT", onContinue);
8987
+ try {
8988
+ process.kill(process.pid, "SIGTSTP");
8989
+ } catch {
8990
+ process.off("SIGCONT", onContinue);
8991
+ state.ui.start();
8992
+ state.ui.requestRender();
8993
+ showError(state, "Unable to suspend in the current terminal");
8994
+ }
8995
+ });
7994
8996
  state.editor.onAction("undo", () => {
7995
8997
  if (state.lastClearedText && state.editor.getText().length === 0) {
7996
8998
  state.editor.setText(state.lastClearedText);
@@ -8105,9 +9107,10 @@ function detectFdPath() {
8105
9107
  function setupAutocomplete(state) {
8106
9108
  const slashCommands = [
8107
9109
  { name: "new", description: "Start a new thread" },
9110
+ { name: "clone", description: "Clone the current thread" },
8108
9111
  { name: "threads", description: "Switch between threads" },
8109
- { name: "models", description: "Configure model (global/thread/mode)" },
8110
- { name: "models:pack", description: "Switch model pack" },
9112
+ { name: "models", description: "Switch model pack" },
9113
+ { name: "custom-providers", description: "Manage custom providers and models" },
8111
9114
  { name: "subagents", description: "Configure subagent model defaults" },
8112
9115
  { name: "om", description: "Configure Observational Memory models" },
8113
9116
  { name: "think", description: "Set thinking (off|low|medium|high|xhigh|status)" },
@@ -8144,6 +9147,7 @@ function setupAutocomplete(state) {
8144
9147
  description: "Toggle YOLO mode (auto-approve all tools)"
8145
9148
  },
8146
9149
  { name: "review", description: "Review a GitHub pull request" },
9150
+ { name: "report-issue", description: "Open or browse mastracode issues" },
8147
9151
  { name: "setup", description: "Re-run the setup wizard" },
8148
9152
  { name: "theme", description: "Switch color theme (auto/dark/light)" },
8149
9153
  { name: "exit", description: "Exit the TUI" },
@@ -8190,6 +9194,9 @@ function setupKeyHandlers(state, callbacks) {
8190
9194
  if (state.pendingApprovalDismiss) {
8191
9195
  state.pendingApprovalDismiss();
8192
9196
  }
9197
+ state.activeInlinePlanApproval = void 0;
9198
+ state.activeInlineQuestion = void 0;
9199
+ state.pendingInlineQuestions.length = 0;
8193
9200
  state.userInitiatedAbort = true;
8194
9201
  state.harness.abort();
8195
9202
  });
@@ -8227,23 +9234,37 @@ async function promptForThreadSelection(state) {
8227
9234
  return;
8228
9235
  }
8229
9236
  const sortedThreads = [...threads].sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
8230
- const mostRecent = sortedThreads[0];
8231
- try {
8232
- await state.harness.switchThread({ threadId: mostRecent.id });
8233
- if (!mostRecent.metadata?.projectPath) {
8234
- await state.harness.setThreadSetting({ key: "projectPath", value: currentPath });
9237
+ if (sortedThreads.length === 1) {
9238
+ const thread = sortedThreads[0];
9239
+ try {
9240
+ await state.harness.switchThread({ threadId: thread.id });
9241
+ if (!thread.metadata?.projectPath) {
9242
+ await state.harness.setThreadSetting({ key: "projectPath", value: currentPath });
9243
+ }
9244
+ return;
9245
+ } catch (error) {
9246
+ if (error instanceof ThreadLockError) {
9247
+ state.pendingNewThread = true;
9248
+ return;
9249
+ }
9250
+ throw error;
8235
9251
  }
8236
- } catch (error) {
8237
- if (error instanceof ThreadLockError) {
8238
- state.pendingNewThread = true;
8239
- state.pendingLockConflict = {
8240
- threadTitle: mostRecent.title || mostRecent.id,
8241
- ownerPid: error.ownerPid
8242
- };
9252
+ }
9253
+ for (const thread of sortedThreads) {
9254
+ try {
9255
+ await state.harness.switchThread({ threadId: thread.id });
9256
+ if (!thread.metadata?.projectPath) {
9257
+ await state.harness.setThreadSetting({ key: "projectPath", value: currentPath });
9258
+ }
8243
9259
  return;
9260
+ } catch (error) {
9261
+ if (error instanceof ThreadLockError) {
9262
+ continue;
9263
+ }
9264
+ throw error;
8244
9265
  }
8245
- throw error;
8246
9266
  }
9267
+ state.pendingNewThread = true;
8247
9268
  }
8248
9269
  async function renderExistingTasks(state) {
8249
9270
  try {
@@ -8307,6 +9328,38 @@ async function handleShellPassthrough(state, command) {
8307
9328
  showError(state, error instanceof Error ? error.message : "Shell command failed");
8308
9329
  }
8309
9330
  }
9331
+ function getClipboardText() {
9332
+ try {
9333
+ if (process.platform === "darwin") {
9334
+ const text = execSync("pbpaste", {
9335
+ encoding: "utf-8",
9336
+ timeout: 3e3,
9337
+ stdio: ["pipe", "pipe", "pipe"]
9338
+ });
9339
+ return text.length > 0 ? text : null;
9340
+ }
9341
+ if (process.platform === "linux") {
9342
+ try {
9343
+ const text = execSync("xclip -selection clipboard -o", {
9344
+ encoding: "utf-8",
9345
+ timeout: 3e3,
9346
+ stdio: ["pipe", "pipe", "pipe"]
9347
+ });
9348
+ return text.length > 0 ? text : null;
9349
+ } catch {
9350
+ const text = execSync("wl-paste", {
9351
+ encoding: "utf-8",
9352
+ timeout: 3e3,
9353
+ stdio: ["pipe", "pipe", "pipe"]
9354
+ });
9355
+ return text.length > 0 ? text : null;
9356
+ }
9357
+ }
9358
+ return null;
9359
+ } catch {
9360
+ return null;
9361
+ }
9362
+ }
8310
9363
  function getClipboardImage() {
8311
9364
  try {
8312
9365
  if (process.platform === "darwin") {
@@ -8469,6 +9522,22 @@ var CustomEditor = class extends Editor {
8469
9522
  return;
8470
9523
  }
8471
9524
  }
9525
+ if (matchesKey(data, "ctrl+v")) {
9526
+ if (this.onImagePaste) {
9527
+ const clipboardImage = getClipboardImage();
9528
+ if (clipboardImage) {
9529
+ this.onImagePaste(clipboardImage);
9530
+ return;
9531
+ }
9532
+ }
9533
+ const clipboardText = getClipboardText();
9534
+ if (clipboardText) {
9535
+ const syntheticPaste = `${PASTE_START}${clipboardText}${PASTE_END}`;
9536
+ super.handleInput(syntheticPaste);
9537
+ return;
9538
+ }
9539
+ return;
9540
+ }
8472
9541
  if (matchesKey(data, "ctrl+c")) {
8473
9542
  const handler = this.actionHandlers.get("clear");
8474
9543
  if (handler) {
@@ -8491,6 +9560,13 @@ var CustomEditor = class extends Editor {
8491
9560
  return;
8492
9561
  }
8493
9562
  if (matchesKey(data, "ctrl+z")) {
9563
+ const handler = this.actionHandlers.get("suspend");
9564
+ if (handler) {
9565
+ handler();
9566
+ return;
9567
+ }
9568
+ }
9569
+ if (matchesKey(data, "alt+z")) {
8494
9570
  const handler = this.actionHandlers.get("undo");
8495
9571
  if (handler) {
8496
9572
  handler();
@@ -8573,11 +9649,12 @@ function createTUIState(options) {
8573
9649
  pendingSubagents: /* @__PURE__ */ new Map(),
8574
9650
  toolOutputExpanded: false,
8575
9651
  hideThinkingBlock: true,
9652
+ quietMode: false,
8576
9653
  // Thread / conversation
8577
9654
  pendingNewThread: false,
8578
- pendingLockConflict: null,
8579
9655
  // Inline interaction
8580
9656
  lastClearedText: "",
9657
+ pendingInlineQuestions: [],
8581
9658
  followUpComponents: [],
8582
9659
  pendingSlashCommands: [],
8583
9660
  pendingApprovalDismiss: null,
@@ -8594,11 +9671,15 @@ function createTUIState(options) {
8594
9671
  }
8595
9672
 
8596
9673
  // src/tui/mastra-tui.ts
9674
+ var UPDATE_RECHECK_INTERVAL_MS = 45 * 60 * 1e3;
8597
9675
  var MastraTUI = class _MastraTUI {
8598
9676
  state;
9677
+ updateCheckTimer = null;
8599
9678
  static DOUBLE_CTRL_C_MS = 500;
8600
9679
  constructor(options) {
8601
9680
  this.state = createTUIState(options);
9681
+ const savedSettings = loadSettings();
9682
+ this.state.quietMode = savedSettings.preferences.quietMode;
8602
9683
  const originalHandleInput = this.state.editor.handleInput.bind(this.state.editor);
8603
9684
  this.state.editor.handleInput = (data) => {
8604
9685
  if (this.state.activeInlinePlanApproval) {
@@ -8701,7 +9782,8 @@ var MastraTUI = class _MastraTUI {
8701
9782
  * Errors are handled via harness events.
8702
9783
  */
8703
9784
  fireMessage(content, images) {
8704
- this.state.harness.sendMessage({ content, images: images ? images : void 0 }).catch((error) => {
9785
+ const files = images?.map((img) => ({ data: img.data, mediaType: img.mimeType }));
9786
+ this.state.harness.sendMessage({ content, files }).catch((error) => {
8705
9787
  showError(this.state, error instanceof Error ? error.message : "Unknown error");
8706
9788
  });
8707
9789
  }
@@ -8714,6 +9796,10 @@ var MastraTUI = class _MastraTUI {
8714
9796
  hookMgr.runSessionEnd().catch(() => {
8715
9797
  });
8716
9798
  }
9799
+ if (this.updateCheckTimer) {
9800
+ clearInterval(this.updateCheckTimer);
9801
+ this.updateCheckTimer = null;
9802
+ }
8717
9803
  if (this.state.unsubscribe) {
8718
9804
  this.state.unsubscribe();
8719
9805
  }
@@ -8745,12 +9831,16 @@ var MastraTUI = class _MastraTUI {
8745
9831
  await renderExistingMessages(this.state);
8746
9832
  await renderExistingTasks(this.state);
8747
9833
  await this.checkClaudeMaxOAuthWarning();
8748
- if (this.state.pendingLockConflict) {
8749
- this.showThreadLockPrompt(this.state.pendingLockConflict.threadTitle, this.state.pendingLockConflict.ownerPid);
8750
- this.state.pendingLockConflict = null;
8751
- } else if (this.shouldShowOnboarding()) {
9834
+ if (this.shouldShowOnboarding()) {
8752
9835
  await this.showOnboarding();
8753
9836
  }
9837
+ await this.checkForUpdate();
9838
+ this.updateCheckTimer = setInterval(() => {
9839
+ void this.checkForUpdate(
9840
+ /* passive */
9841
+ true
9842
+ );
9843
+ }, UPDATE_RECHECK_INTERVAL_MS);
8754
9844
  }
8755
9845
  async refreshModelAuthStatus() {
8756
9846
  this.state.modelAuthStatus = await this.state.harness.getCurrentModelAuthStatus();
@@ -8769,11 +9859,60 @@ var MastraTUI = class _MastraTUI {
8769
9859
  }
8770
9860
  async handleEvent(event) {
8771
9861
  await dispatchEvent(event, this.getEventContext(), this.state);
9862
+ if (event.type === "thread_created") {
9863
+ await this.syncThreadActivePackMetadata(event.thread);
9864
+ } else if (event.type === "thread_changed") {
9865
+ await this.syncThreadActivePackMetadata();
9866
+ }
8772
9867
  if (event.type === "agent_end") {
8773
9868
  const stopReason = event.reason === "aborted" ? "aborted" : event.reason === "error" ? "error" : "complete";
8774
9869
  await this.runStopHook(stopReason);
8775
9870
  }
8776
9871
  }
9872
+ async buildProviderAccess() {
9873
+ const models = await this.state.harness.listAvailableModels();
9874
+ const hasEnv = (provider) => models.some((m) => m.provider === provider && m.hasApiKey);
9875
+ const accessLevel = (provider, oauthId) => {
9876
+ if (this.state.authStorage?.isLoggedIn(oauthId)) return "oauth";
9877
+ if (hasEnv(provider)) return "apikey";
9878
+ return false;
9879
+ };
9880
+ const access = {
9881
+ anthropic: accessLevel("anthropic", "anthropic"),
9882
+ openai: accessLevel("openai", "openai-codex"),
9883
+ cerebras: hasEnv("cerebras") ? "apikey" : false,
9884
+ google: hasEnv("google") ? "apikey" : false,
9885
+ deepseek: hasEnv("deepseek") ? "apikey" : false
9886
+ };
9887
+ const seen = new Set(Object.keys(access));
9888
+ for (const m of models) {
9889
+ if (!seen.has(m.provider) && m.hasApiKey) {
9890
+ access[m.provider] = "apikey";
9891
+ seen.add(m.provider);
9892
+ }
9893
+ }
9894
+ return access;
9895
+ }
9896
+ async syncThreadActivePackMetadata(thread) {
9897
+ const settings = loadSettings();
9898
+ const currentThreadId = this.state.harness.getCurrentThreadId();
9899
+ if (!currentThreadId) return;
9900
+ const resolvedThread = thread?.id === currentThreadId ? thread : (await this.state.harness.listThreads()).find((t) => t.id === currentThreadId);
9901
+ const access = await this.buildProviderAccess();
9902
+ const packs = getAvailableModePacks(access, settings.customModelPacks).filter((p) => p.id !== "custom");
9903
+ const resolvedPackId = resolveThreadActiveModelPackId(
9904
+ settings,
9905
+ packs,
9906
+ resolvedThread?.metadata
9907
+ );
9908
+ if (resolvedPackId && settings.models.activeModelPackId !== resolvedPackId) {
9909
+ const fresh = loadSettings();
9910
+ if (fresh.models.activeModelPackId !== resolvedPackId) {
9911
+ fresh.models.activeModelPackId = resolvedPackId;
9912
+ saveSettings(fresh);
9913
+ }
9914
+ }
9915
+ }
8777
9916
  showHookWarnings(event, warnings) {
8778
9917
  for (const warning of warnings) {
8779
9918
  showInfo(this.state, `[${event}] ${warning}`);
@@ -8839,42 +9978,6 @@ var MastraTUI = class _MastraTUI {
8839
9978
  };
8840
9979
  });
8841
9980
  }
8842
- /**
8843
- * Show an inline prompt when a thread is locked by another process.
8844
- * User can create a new thread (y) or exit (n).
8845
- */
8846
- showThreadLockPrompt(threadTitle, ownerPid) {
8847
- const questionComponent = new AskQuestionInlineComponent(
8848
- {
8849
- question: `Thread "${threadTitle}" is locked by pid ${ownerPid}. Create a new thread?`,
8850
- options: [
8851
- { label: "Yes", description: "Start a new thread" },
8852
- { label: "No", description: "Exit" }
8853
- ],
8854
- formatResult: (answer) => answer === "Yes" ? "Thread created" : "Exiting.",
8855
- onSubmit: async (answer) => {
8856
- this.state.activeInlineQuestion = void 0;
8857
- if (answer.toLowerCase().startsWith("y")) {
8858
- if (this.shouldShowOnboarding()) {
8859
- await this.showOnboarding();
8860
- }
8861
- } else {
8862
- process.exit(0);
8863
- }
8864
- },
8865
- onCancel: () => {
8866
- this.state.activeInlineQuestion = void 0;
8867
- process.exit(0);
8868
- }
8869
- },
8870
- this.state.ui
8871
- );
8872
- this.state.activeInlineQuestion = questionComponent;
8873
- this.state.chatContainer.addChild(questionComponent);
8874
- this.state.chatContainer.addChild(new Spacer(1));
8875
- this.state.ui.requestRender();
8876
- this.state.chatContainer.invalidate();
8877
- }
8878
9981
  /**
8879
9982
  * One-time startup check: if the user has Anthropic OAuth credentials and
8880
9983
  * hasn't yet acknowledged the Claude Max ToS warning, show it now.
@@ -9016,23 +10119,7 @@ var MastraTUI = class _MastraTUI {
9016
10119
  value: p.id,
9017
10120
  loggedIn: this.state.authStorage?.isLoggedIn(p.id) ?? false
9018
10121
  }));
9019
- const buildAccess = async () => {
9020
- const models = await this.state.harness.listAvailableModels();
9021
- const hasEnv = (provider) => models.some((m) => m.provider === provider && m.hasApiKey);
9022
- const accessLevel = (provider, oauthId) => {
9023
- if (this.state.authStorage?.isLoggedIn(oauthId)) return "oauth";
9024
- if (hasEnv(provider)) return "apikey";
9025
- return false;
9026
- };
9027
- return {
9028
- anthropic: accessLevel("anthropic", "anthropic"),
9029
- openai: accessLevel("openai", "openai-codex"),
9030
- cerebras: hasEnv("cerebras") ? "apikey" : false,
9031
- google: hasEnv("google") ? "apikey" : false,
9032
- deepseek: hasEnv("deepseek") ? "apikey" : false
9033
- };
9034
- };
9035
- const access = await buildAccess();
10122
+ const access = await this.buildProviderAccess();
9036
10123
  const hasProviderAccess = Object.values(access).some(Boolean);
9037
10124
  const savedSettings = loadSettings();
9038
10125
  const modePacks = getAvailableModePacks(access, savedSettings.customModelPacks);
@@ -9077,7 +10164,7 @@ var MastraTUI = class _MastraTUI {
9077
10164
  }
9078
10165
  this.performLogin(providerId).then(async () => {
9079
10166
  try {
9080
- const updatedAccess = await buildAccess();
10167
+ const updatedAccess = await this.buildProviderAccess();
9081
10168
  const updatedHasAccess = Object.values(updatedAccess).some(Boolean);
9082
10169
  component.updateModePacks(getAvailableModePacks(updatedAccess, savedSettings.customModelPacks));
9083
10170
  component.updateOmPacks(getAvailableOmPacks(updatedAccess));
@@ -9158,30 +10245,32 @@ var MastraTUI = class _MastraTUI {
9158
10245
  settings.onboarding.completedAt = (/* @__PURE__ */ new Date()).toISOString();
9159
10246
  settings.onboarding.skippedAt = null;
9160
10247
  settings.onboarding.version = ONBOARDING_VERSION;
9161
- settings.onboarding.modePackId = modePack.id;
9162
10248
  settings.onboarding.omPackId = omPack.id;
9163
10249
  const modeDefaults = {};
9164
10250
  for (const mode of modes) {
9165
10251
  const modelId = modePack.models[mode.id];
9166
10252
  if (modelId) modeDefaults[mode.id] = modelId;
9167
10253
  }
9168
- if (modePack.id === "custom") {
9169
- const idx = settings.customModelPacks.findIndex((p) => p.name === "Setup");
9170
- const entry = { name: "Setup", models: modeDefaults, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
10254
+ let activeModePackId = modePack.id;
10255
+ if (modePack.id === "custom" || modePack.id.startsWith("custom:")) {
10256
+ const customName = modePack.id === "custom" ? modePack.name?.trim() || "Custom" : modePack.id.slice("custom:".length) || "Custom";
10257
+ activeModePackId = `custom:${customName}`;
10258
+ const entry = { name: customName, models: modeDefaults, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
10259
+ const idx = settings.customModelPacks.findIndex((p) => p.name === customName);
9171
10260
  if (idx >= 0) {
9172
10261
  settings.customModelPacks[idx] = entry;
9173
10262
  } else {
9174
10263
  settings.customModelPacks.push(entry);
9175
10264
  }
9176
- settings.models.activeModelPackId = "custom:Setup";
9177
- settings.models.modeDefaults = modeDefaults;
9178
- } else if (modePack.id.startsWith("custom:")) {
9179
- settings.models.activeModelPackId = modePack.id;
9180
10265
  settings.models.modeDefaults = modeDefaults;
9181
10266
  } else {
9182
- settings.models.activeModelPackId = modePack.id;
9183
10267
  settings.models.modeDefaults = {};
9184
10268
  }
10269
+ settings.onboarding.modePackId = activeModePackId;
10270
+ settings.models.activeModelPackId = activeModePackId;
10271
+ if (harness.getCurrentThreadId()) {
10272
+ await harness.setThreadSetting({ key: THREAD_ACTIVE_MODEL_PACK_ID_KEY, value: activeModePackId });
10273
+ }
9185
10274
  settings.models.activeOmPackId = omPack.id;
9186
10275
  settings.models.omModelOverride = omPack.id === "custom" ? omPack.modelId : null;
9187
10276
  settings.preferences.yolo = result.yolo;
@@ -9198,6 +10287,92 @@ var MastraTUI = class _MastraTUI {
9198
10287
  }
9199
10288
  return true;
9200
10289
  }
10290
+ // ===========================================================================
10291
+ // Auto-Update
10292
+ // ===========================================================================
10293
+ /**
10294
+ * Check npm for a newer version and prompt the user to update.
10295
+ * - If the user previously dismissed this version, show a passive note instead.
10296
+ * - If the fetch fails or we're already up-to-date, silently return.
10297
+ * @param passive When true, only show an info message (used for periodic rechecks).
10298
+ */
10299
+ async checkForUpdate(passive = false) {
10300
+ const currentVersion = this.state.options.version;
10301
+ if (!currentVersion) return;
10302
+ const latestVersion = await fetchLatestVersion();
10303
+ if (!latestVersion || !isNewerVersion(currentVersion, latestVersion)) return;
10304
+ const pm = await detectPackageManager();
10305
+ if (passive) {
10306
+ const cmd = getInstallCommand(pm);
10307
+ showInfo(
10308
+ this.state,
10309
+ `Update available: v${latestVersion} (current: v${currentVersion}). Run \`${cmd}\` to update.`
10310
+ );
10311
+ return;
10312
+ }
10313
+ const settings = loadSettings();
10314
+ if (settings.updateDismissedVersion && !isNewerVersion(settings.updateDismissedVersion, latestVersion)) {
10315
+ const cmd = getInstallCommand(pm);
10316
+ showInfo(
10317
+ this.state,
10318
+ `Update available: v${latestVersion} (current: v${currentVersion}). Run \`${cmd}\` to update.`
10319
+ );
10320
+ return;
10321
+ }
10322
+ await this.showUpdatePrompt(currentVersion, latestVersion, pm);
10323
+ }
10324
+ /**
10325
+ * Show an inline Y/N prompt offering to auto-update.
10326
+ */
10327
+ showUpdatePrompt(currentVersion, latestVersion, pm) {
10328
+ return new Promise((resolve2) => {
10329
+ const questionComponent = new AskQuestionInlineComponent(
10330
+ {
10331
+ question: `A new version of Mastra Code is available: v${latestVersion} (current: v${currentVersion}). Would you like to update now?`,
10332
+ options: [
10333
+ { label: "Yes", description: "Update and restart" },
10334
+ { label: "No", description: "Skip this version" }
10335
+ ],
10336
+ formatResult: (answer) => answer === "Yes" ? "Updating\u2026" : "Update skipped.",
10337
+ onSubmit: async (answer) => {
10338
+ this.state.activeInlineQuestion = void 0;
10339
+ if (answer === "Yes") {
10340
+ showInfo(this.state, `Updating to v${latestVersion}\u2026`);
10341
+ const ok = await runUpdate(pm, latestVersion);
10342
+ if (ok) {
10343
+ showInfo(this.state, `Updated to v${latestVersion}. Please restart Mastra Code.`);
10344
+ this.stop();
10345
+ process.exit(0);
10346
+ } else {
10347
+ const cmd = getInstallCommand(pm, latestVersion);
10348
+ showError(this.state, `Auto-update failed. Run \`${cmd}\` manually.`);
10349
+ }
10350
+ } else {
10351
+ const settings = loadSettings();
10352
+ settings.updateDismissedVersion = latestVersion;
10353
+ saveSettings(settings);
10354
+ const cmd = getInstallCommand(pm);
10355
+ showInfo(this.state, `Update skipped. Run \`${cmd}\` to update manually.`);
10356
+ }
10357
+ resolve2();
10358
+ },
10359
+ onCancel: () => {
10360
+ this.state.activeInlineQuestion = void 0;
10361
+ const settings = loadSettings();
10362
+ settings.updateDismissedVersion = latestVersion;
10363
+ saveSettings(settings);
10364
+ resolve2();
10365
+ }
10366
+ },
10367
+ this.state.ui
10368
+ );
10369
+ this.state.activeInlineQuestion = questionComponent;
10370
+ this.state.chatContainer.addChild(questionComponent);
10371
+ this.state.chatContainer.addChild(new Spacer(1));
10372
+ this.state.ui.requestRender();
10373
+ this.state.chatContainer.invalidate();
10374
+ });
10375
+ }
9201
10376
  };
9202
10377
  var LoginSelectorComponent = class extends Box {
9203
10378
  listContainer;
@@ -9266,6 +10441,6 @@ var LoginSelectorComponent = class extends Box {
9266
10441
  }
9267
10442
  };
9268
10443
 
9269
- export { AssistantMessageComponent, LoginDialogComponent, LoginSelectorComponent, MastraTUI, ModelSelectorComponent, OMProgressComponent, ToolExecutionComponentEnhanced, UserMessageComponent, createTUIState, detectTerminalTheme, formatOMStatus };
9270
- //# sourceMappingURL=chunk-X3BGE7CL.js.map
9271
- //# sourceMappingURL=chunk-X3BGE7CL.js.map
10444
+ export { AssistantMessageComponent, LoginDialogComponent, LoginSelectorComponent, MastraTUI, ModelSelectorComponent, OMProgressComponent, ToolExecutionComponentEnhanced, UserMessageComponent, createTUIState, detectTerminalTheme, formatOMStatus, getCurrentVersion };
10445
+ //# sourceMappingURL=chunk-YQNZ7DHQ.js.map
10446
+ //# sourceMappingURL=chunk-YQNZ7DHQ.js.map