maistro 1.2.18 → 1.2.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/app.d.ts CHANGED
@@ -185,6 +185,14 @@ export declare class MaistroApp {
185
185
  * Handle settings command - show settings menu
186
186
  */
187
187
  private handleSettings;
188
+ /**
189
+ * Handle webhook URL configuration in settings
190
+ */
191
+ private handleWebhookSettings;
192
+ /**
193
+ * Prompt user to enter a webhook URL using InputBox
194
+ */
195
+ private promptWebhookUrl;
188
196
  /**
189
197
  * Show manual Cmd+V configuration instructions
190
198
  */
@@ -233,5 +241,9 @@ export declare class MaistroApp {
233
241
  private renderExecutionSection;
234
242
  private handleRun;
235
243
  private executeWithUnifiedView;
244
+ /**
245
+ * Get a display name for the project (folder name, or "projectName (folder)" if they differ)
246
+ */
247
+ private getProjectDisplayName;
236
248
  }
237
249
  //# sourceMappingURL=app.d.ts.map
package/dist/app.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAmOA,qBAAa,UAAU;IACrB,OAAO,CAAC,EAAE,CAAgB;IAC1B,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,aAAa,CAAS;IAG9B,OAAO,CAAC,cAAc,CAA+B;gBAEzC,WAAW,GAAE,MAAY;IAsBrC,OAAO,CAAC,eAAe;IASvB;;;OAGG;YACW,qBAAqB;IA+CnC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkH5B;;;OAGG;YACW,iBAAiB;IAiE/B;;OAEG;IACH,OAAO,CAAC,eAAe;IAUvB;;;OAGG;IACG,YAAY,CAAC,SAAS,UAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAqFtD;;;OAGG;YACW,iBAAiB;IAwD/B;;OAEG;YACW,sBAAsB;IAsCpC;;;OAGG;YACW,iBAAiB;IAoB/B;;;OAGG;YACW,oBAAoB;IAkClC;;OAEG;YACW,qBAAqB;IA4BnC;;OAEG;YACW,gBAAgB;IAoB9B;;;OAGG;YACW,qBAAqB;IAoEnC;;;OAGG;YACW,eAAe;IAwJ7B;;;OAGG;YACW,gBAAgB;IAiD9B;;;OAGG;YACW,uBAAuB;IAyErC;;OAEG;YACW,4BAA4B;IAkE1C;;OAEG;YACW,oBAAoB;IAiElC;;OAEG;IACH,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,UAAU;IAQlB;;;;;;OAMG;IACH,OAAO,CAAC,qBAAqB;IA2J7B,OAAO,CAAC,iBAAiB,CAAM;IAC/B,OAAO,CAAC,gBAAgB,CAAK;IAE7B;;;OAGG;YACW,YAAY;IAyT1B;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA2CxB;;OAEG;YACW,aAAa;IAsmB3B;;;OAGG;YACW,iBAAiB;IAgI/B;;OAEG;YACW,cAAc;IAwU5B,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,YAAY,CAAM;IAC1B,OAAO,CAAC,aAAa,CAAS;IAE9B;;OAEG;YACW,gBAAgB;IAkB9B;;OAEG;YACW,gBAAgB;IAe9B;;;OAGG;YACW,WAAW;IAsEzB;;;OAGG;YACW,mBAAmB;IA8BjC;;;OAGG;YACW,cAAc;IAkB5B;;;;OAIG;YACW,6BAA6B;IAqI3C;;;OAGG;YACW,aAAa;IA+B3B;;;;OAIG;YACW,sBAAsB;IA8MpC;;;;OAIG;YACW,sBAAsB;YA2GtB,UAAU;IA8HxB,OAAO,CAAC,YAAY;IAmFpB;;;OAGG;YACW,eAAe;IAqD7B;;OAEG;YACW,cAAc;IAsO5B;;OAEG;YACW,oBAAoB;IAwBlC;;OAEG;YACW,eAAe;IAU7B;;OAEG;YACW,wBAAwB;YAsBxB,gBAAgB;IA4B9B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsD;IAEpF;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAqB3B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAO1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAS1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAKzB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAqB5B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAQ/B;;OAEG;IACH,OAAO,CAAC,eAAe;IASvB;;OAEG;IACH,OAAO,CAAC,sBAAsB;YAoEhB,SAAS;YAiIT,sBAAsB;CAkcrC"}
1
+ {"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAwOA,qBAAa,UAAU;IACrB,OAAO,CAAC,EAAE,CAAgB;IAC1B,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,aAAa,CAAS;IAG9B,OAAO,CAAC,cAAc,CAA+B;gBAEzC,WAAW,GAAE,MAAY;IAsBrC,OAAO,CAAC,eAAe;IASvB;;;OAGG;YACW,qBAAqB;IA+CnC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAkH5B;;;OAGG;YACW,iBAAiB;IAiE/B;;OAEG;IACH,OAAO,CAAC,eAAe;IAUvB;;;OAGG;IACG,YAAY,CAAC,SAAS,UAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAqFtD;;;OAGG;YACW,iBAAiB;IAwD/B;;OAEG;YACW,sBAAsB;IAsCpC;;;OAGG;YACW,iBAAiB;IAoB/B;;;OAGG;YACW,oBAAoB;IAkClC;;OAEG;YACW,qBAAqB;IA4BnC;;OAEG;YACW,gBAAgB;IAoB9B;;;OAGG;YACW,qBAAqB;IAoEnC;;;OAGG;YACW,eAAe;IAiJ7B;;;OAGG;YACW,gBAAgB;IAiD9B;;;OAGG;YACW,uBAAuB;IAyErC;;OAEG;YACW,4BAA4B;IAkE1C;;OAEG;YACW,oBAAoB;IAiElC;;OAEG;IACH,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,UAAU;IAQlB;;;;;;OAMG;IACH,OAAO,CAAC,qBAAqB;IA2J7B,OAAO,CAAC,iBAAiB,CAAM;IAC/B,OAAO,CAAC,gBAAgB,CAAK;IAE7B;;;OAGG;YACW,YAAY;IAyT1B;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA2CxB;;OAEG;YACW,aAAa;IAsmB3B;;;OAGG;YACW,iBAAiB;IAgI/B;;OAEG;YACW,cAAc;IAwU5B,OAAO,CAAC,YAAY,CAAgB;IACpC,OAAO,CAAC,YAAY,CAAM;IAC1B,OAAO,CAAC,aAAa,CAAS;IAE9B;;OAEG;YACW,gBAAgB;IAkB9B;;OAEG;YACW,gBAAgB;IAe9B;;;OAGG;YACW,WAAW;IAsEzB;;;OAGG;YACW,mBAAmB;IA8BjC;;;OAGG;YACW,cAAc;IAkB5B;;;;OAIG;YACW,6BAA6B;IAqI3C;;;OAGG;YACW,aAAa;IA+B3B;;;;OAIG;YACW,sBAAsB;IA6NpC;;;;OAIG;YACW,sBAAsB;YAmHtB,UAAU;IAsIxB,OAAO,CAAC,YAAY;IAmFpB;;;OAGG;YACW,eAAe;IAqD7B;;OAEG;YACW,cAAc;IAgQ5B;;OAEG;YACW,qBAAqB;IA0CnC;;OAEG;YACW,gBAAgB;IA4C9B;;OAEG;YACW,oBAAoB;IAwBlC;;OAEG;YACW,eAAe;IAU7B;;OAEG;YACW,wBAAwB;YAsBxB,gBAAgB;IA4B9B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsD;IAEpF;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAqB3B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAO1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAS1B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAKzB;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAqB5B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAQ/B;;OAEG;IACH,OAAO,CAAC,eAAe;IASvB;;OAEG;IACH,OAAO,CAAC,sBAAsB;YAoEhB,SAAS;YAiIT,sBAAsB;IA8dpC;;OAEG;IACH,OAAO,CAAC,qBAAqB;CAc9B"}
package/dist/app.js CHANGED
@@ -15,7 +15,8 @@ import { buildHeaderWithLogo } from './logo.js';
15
15
  import { addImageFromFile, addImageFromUrl, addImageFromPaste, getImages, deleteImage, looksLikeImagePath, looksLikeImageUrl, formatImageSize, sanitizeImageId, generatePasteImageId, extractImageUrls, processDescriptionImageUrls, } from './imageManager.js';
16
16
  import { InputBox, getDisplayWidth, charIndexAtDisplayColumn } from './inputBox.js';
17
17
  import { startCaffeinate, stopCaffeinate, isCaffeinateAvailable, isCaffeinateRunning } from './caffeinate.js';
18
- import { getPreventSleep, setPreventSleep, getConfigPath, detectTerminal, getShiftEnterEnabled, setShiftEnterEnabled, configureTerminalShiftEnter, removeTerminalShiftEnter, getNewlineHint, getCmdVPasteEnabled, setCmdVPasteEnabled, configureTerminalCmdVPaste, removeTerminalCmdVPaste, getCmdVPasteInstructions, validateTerminalConfig, } from './config.js';
18
+ import { sendNotification, testWebhook } from './notifications.js';
19
+ import { getPreventSleep, setPreventSleep, getConfigPath, detectTerminal, getShiftEnterEnabled, setShiftEnterEnabled, configureTerminalShiftEnter, removeTerminalShiftEnter, getNewlineHint, getCmdVPasteEnabled, setCmdVPasteEnabled, configureTerminalCmdVPaste, removeTerminalCmdVPaste, getCmdVPasteInstructions, validateTerminalConfig, getSoundNotifications, setSoundNotifications, getWebhookUrl, setWebhookUrl, } from './config.js';
19
20
  import { hasClipboardImage, saveClipboardImage, cleanupTempImage } from './clipboard.js';
20
21
  import { isGitRepo } from './git.js';
21
22
  import { UI, TipsManager } from './constants.js';
@@ -703,19 +704,6 @@ export class MaistroApp {
703
704
  * Uses full-screen approach for each step
704
705
  */
705
706
  async handleDiscovery(goal) {
706
- // Step 1: Ask if user wants discovery (full-screen)
707
- const skipChoice = await this.fullScreenMenu({
708
- title: 'Project Discovery',
709
- subtitle: 'Would you like to answer a few questions to refine your project plan?',
710
- items: [
711
- { key: 'd', label: 'Discover', description: 'Answer questions to refine the plan (Recommended)' },
712
- { key: 's', label: 'Skip', description: 'Go straight to task planning' },
713
- ],
714
- exitOnDoubleEscape: true, // Allow exit via double-escape during discovery
715
- });
716
- if (skipChoice === 's' || skipChoice === null) {
717
- return null; // Skip discovery
718
- }
719
707
  // Show analyzing screen with spinner (with escape-to-exit support)
720
708
  setStatusState('working');
721
709
  this.ui.clear();
@@ -755,6 +743,13 @@ export class MaistroApp {
755
743
  questions: result.questions.map(q => ({ id: q.id, question: q.question, options: q.options })),
756
744
  assumptions: result.assumptions,
757
745
  });
746
+ // Notify user that input is needed
747
+ await sendNotification({
748
+ event: 'attention',
749
+ title: 'Discovery: input needed',
750
+ detail: `${result.questions.length} question${result.questions.length > 1 ? 's' : ''} to answer`,
751
+ project: this.getProjectDisplayName(),
752
+ });
758
753
  // Ask each question one at a time (full-screen for each)
759
754
  const answers = {};
760
755
  for (let i = 0; i < result.questions.length; i++) {
@@ -2824,6 +2819,14 @@ export class MaistroApp {
2824
2819
  this.ui.clear();
2825
2820
  this.showHeader();
2826
2821
  console.log(`\x1b[1m── Discussion ──\x1b[0m\n\x1b[90mYou: ${userInput.length > 100 ? userInput.slice(0, 97) + '...' : userInput}\x1b[0m\n`);
2822
+ // Run discovery phase for the new request (same as initial project setup)
2823
+ // This detects external dependencies, asks clarifying questions, and prompts for secrets
2824
+ const discoveryResult = await this.handleDiscovery(userInput);
2825
+ let discoveryContext = '';
2826
+ if (discoveryResult && discoveryResult.confirmed) {
2827
+ const context = buildProjectContext(userInput, discoveryResult);
2828
+ discoveryContext = formatContextForDecomposition(context);
2829
+ }
2827
2830
  let hasOutput = false;
2828
2831
  // Set up abort controller for cancellation
2829
2832
  const abortController = new AbortController();
@@ -2881,9 +2884,13 @@ export class MaistroApp {
2881
2884
  }
2882
2885
  };
2883
2886
  process.stdin.on('data', handleCancelKey);
2887
+ // Include discovery context in the planning request if available
2888
+ const enrichedMessage = discoveryContext
2889
+ ? `${userInput}\n\nDiscovery Context:\n${discoveryContext}`
2890
+ : userInput;
2884
2891
  const result = await planTasks({
2885
2892
  goal,
2886
- userMessage: userInput,
2893
+ userMessage: enrichedMessage,
2887
2894
  currentTasks,
2888
2895
  conversationHistory: [],
2889
2896
  cwd: this.projectPath,
@@ -3077,6 +3084,13 @@ ${detection.projectContext}`;
3077
3084
  }
3078
3085
  console.log(`\n\x1b[32m✓ Plan created with ${tasksWithImages.length} tasks\x1b[0m\n`);
3079
3086
  getLogger()?.info('Plan created and finalized', { taskCount: tasksWithImages.length });
3087
+ // Notify user that plan is ready for review
3088
+ await sendNotification({
3089
+ event: 'attention',
3090
+ title: 'Plan ready for review',
3091
+ detail: `${tasksWithImages.length} tasks created`,
3092
+ project: this.getProjectDisplayName(),
3093
+ });
3080
3094
  }
3081
3095
  catch (err) {
3082
3096
  // Re-throw ExitRequestedError to propagate exit signal
@@ -3185,6 +3199,13 @@ ${detection.projectContext}`;
3185
3199
  }
3186
3200
  console.log(`\n\x1b[32m✓ Project initialized with ${tasksWithImages.length} tasks\x1b[0m\n`);
3187
3201
  getLogger()?.info('Plan created and finalized', { taskCount: tasksWithImages.length });
3202
+ // Notify user that plan is ready for review
3203
+ await sendNotification({
3204
+ event: 'attention',
3205
+ title: 'Plan ready for review',
3206
+ detail: `${tasksWithImages.length} tasks created`,
3207
+ project: this.getProjectDisplayName(),
3208
+ });
3188
3209
  }
3189
3210
  catch (err) {
3190
3211
  // Re-throw ExitRequestedError to propagate exit signal
@@ -3394,6 +3415,20 @@ ${detection.projectContext}`;
3394
3415
  description: `${terminalInfo.name} - use /paste command instead`,
3395
3416
  });
3396
3417
  }
3418
+ // Sound notification toggle
3419
+ const soundEnabled = getSoundNotifications();
3420
+ menuItems.push({
3421
+ key: 'sound',
3422
+ label: `Sound notifications: ${soundEnabled ? '\x1b[32mON\x1b[0m' : '\x1b[90mOFF\x1b[0m'}`,
3423
+ description: 'Play a chime when execution completes or needs attention',
3424
+ });
3425
+ // Webhook notification
3426
+ const webhookUrl = getWebhookUrl();
3427
+ menuItems.push({
3428
+ key: 'webhook',
3429
+ label: `Webhook notifications: ${webhookUrl ? '\x1b[32mConfigured\x1b[0m' : '\x1b[90mOFF\x1b[0m'}`,
3430
+ description: webhookUrl ? 'Update or remove webhook URL' : 'Configure a webhook URL for notifications',
3431
+ });
3397
3432
  menuItems.push({
3398
3433
  key: 'back',
3399
3434
  label: 'Back',
@@ -3530,8 +3565,107 @@ ${detection.projectContext}`;
3530
3565
  // Show manual instructions for Cmd+V
3531
3566
  await this.showCmdVInstructions();
3532
3567
  }
3568
+ if (choice === 'sound') {
3569
+ const newValue = !soundEnabled;
3570
+ setSoundNotifications(newValue);
3571
+ // Loop redraws with updated value
3572
+ }
3573
+ if (choice === 'webhook') {
3574
+ await this.handleWebhookSettings();
3575
+ }
3576
+ }
3577
+ }
3578
+ /**
3579
+ * Handle webhook URL configuration in settings
3580
+ */
3581
+ async handleWebhookSettings() {
3582
+ const currentUrl = getWebhookUrl();
3583
+ if (currentUrl) {
3584
+ // Already configured — show sub-menu
3585
+ this.ui.clear();
3586
+ this.showHeader();
3587
+ console.log('\x1b[1m── Settings: Webhook Notifications ──\x1b[0m\n');
3588
+ console.log(`\x1b[90mCurrent URL: ${currentUrl}\x1b[0m\n`);
3589
+ const subMenuItems = [
3590
+ { key: 'update', label: 'Update URL', description: 'Enter a new webhook URL' },
3591
+ { key: 'test', label: 'Test notification', description: 'Send a test message to the webhook' },
3592
+ { key: 'remove', label: 'Remove', description: 'Disable webhook notifications' },
3593
+ { key: 'cancel', label: 'Cancel', description: 'Return to settings' },
3594
+ ];
3595
+ const subChoice = await this.ui.quickPick(subMenuItems, 'Webhook options:', undefined, { showInput: false });
3596
+ if (subChoice === 'update') {
3597
+ await this.promptWebhookUrl();
3598
+ }
3599
+ else if (subChoice === 'test') {
3600
+ this.ui.clear();
3601
+ this.showHeader();
3602
+ console.log('\x1b[1m── Settings: Test Webhook ──\x1b[0m\n');
3603
+ console.log('Sending test notification...\n');
3604
+ const result = await testWebhook(currentUrl);
3605
+ if (result.success) {
3606
+ console.log('\x1b[32m✓ Test notification sent successfully!\x1b[0m');
3607
+ }
3608
+ else {
3609
+ console.log(`\x1b[31m✗ Failed: ${result.error}\x1b[0m`);
3610
+ }
3611
+ console.log('\n\x1b[90mPress any key to continue...\x1b[0m');
3612
+ await this.waitForKeypress();
3613
+ }
3614
+ else if (subChoice === 'remove') {
3615
+ setWebhookUrl(undefined);
3616
+ }
3617
+ }
3618
+ else {
3619
+ // Not configured — prompt for URL
3620
+ await this.promptWebhookUrl();
3533
3621
  }
3534
3622
  }
3623
+ /**
3624
+ * Prompt user to enter a webhook URL using InputBox
3625
+ */
3626
+ async promptWebhookUrl() {
3627
+ return new Promise((resolve) => {
3628
+ const inputBox = new InputBox({
3629
+ placeholder: 'https://hooks.slack.com/services/...',
3630
+ hint: 'Enter to save · Esc to cancel',
3631
+ onRedraw: () => {
3632
+ this.ui.clear();
3633
+ this.showHeader();
3634
+ console.log('\x1b[1m── Settings: Webhook URL ──\x1b[0m\n');
3635
+ console.log('Enter your webhook URL for notifications.');
3636
+ console.log('Works with Slack, Discord, or any endpoint that accepts JSON POST.\n');
3637
+ console.log('\x1b[90mSlack: Workspace Settings → Manage Apps → Custom Integrations → Incoming WebHooks\x1b[0m\n');
3638
+ },
3639
+ onSubmit: (value) => {
3640
+ const url = value.trim();
3641
+ if (!url) {
3642
+ resolve();
3643
+ return;
3644
+ }
3645
+ if (!url.startsWith('https://')) {
3646
+ this.ui.clear();
3647
+ this.showHeader();
3648
+ console.log('\x1b[1m── Settings: Webhook URL ──\x1b[0m\n');
3649
+ console.log('\x1b[31m✗ URL must start with https://\x1b[0m');
3650
+ console.log('\n\x1b[90mPress any key to continue...\x1b[0m');
3651
+ this.waitForKeypress().then(() => resolve());
3652
+ return;
3653
+ }
3654
+ setWebhookUrl(url);
3655
+ this.ui.clear();
3656
+ this.showHeader();
3657
+ console.log('\x1b[1m── Settings ──\x1b[0m\n');
3658
+ console.log('\x1b[32m✓ Webhook URL saved!\x1b[0m');
3659
+ console.log('\n\x1b[90mPress any key to continue...\x1b[0m');
3660
+ this.waitForKeypress().then(() => resolve());
3661
+ },
3662
+ onCancel: () => {
3663
+ resolve();
3664
+ },
3665
+ });
3666
+ inputBox.start();
3667
+ });
3668
+ }
3535
3669
  /**
3536
3670
  * Show manual Cmd+V configuration instructions
3537
3671
  */
@@ -4293,8 +4427,54 @@ ${detection.projectContext}`;
4293
4427
  getLogger()?.error('Execution failed with full error', { error: result.error });
4294
4428
  }
4295
4429
  }
4430
+ // Send notifications (sound + webhook)
4431
+ // Skip notification if no tasks were actually executed this run (all were already done)
4432
+ const tasksExecutedThisRun = result.completedTasks - alreadyCompleted;
4433
+ const project = this.getProjectDisplayName();
4434
+ if (result.success && tasksExecutedThisRun > 0) {
4435
+ await sendNotification({
4436
+ event: 'completed',
4437
+ title: 'All tasks completed',
4438
+ detail: `${result.completedTasks}/${result.totalTasks} tasks completed successfully`,
4439
+ project,
4440
+ });
4441
+ }
4442
+ else if (result.blockedTask) {
4443
+ await sendNotification({
4444
+ event: 'attention',
4445
+ title: `Task failed: ${result.blockedTask.title}`,
4446
+ detail: result.error ? truncateError(result.error) : undefined,
4447
+ project,
4448
+ });
4449
+ }
4450
+ else if (!this.shouldStop) {
4451
+ // User-initiated pause doesn't need a notification
4452
+ await sendNotification({
4453
+ event: 'attention',
4454
+ title: 'Execution failed',
4455
+ detail: result.error ? truncateError(result.error) : undefined,
4456
+ project,
4457
+ });
4458
+ }
4296
4459
  // Clear cached orchestrator
4297
4460
  this.orchestrator = null;
4298
4461
  }
4462
+ /**
4463
+ * Get a display name for the project (folder name, or "projectName (folder)" if they differ)
4464
+ */
4465
+ getProjectDisplayName() {
4466
+ const folderName = basename(this.projectPath);
4467
+ try {
4468
+ const orchestrator = this.getOrchestrator();
4469
+ const projectName = orchestrator.getProjectName();
4470
+ if (projectName && projectName !== folderName) {
4471
+ return `${projectName} (${folderName})`;
4472
+ }
4473
+ }
4474
+ catch {
4475
+ // Orchestrator may not be loaded yet
4476
+ }
4477
+ return folderName;
4478
+ }
4299
4479
  }
4300
4480
  //# sourceMappingURL=app.js.map