yggtree 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -162,7 +162,7 @@ Sometimes you don't want to "commit to a branch" yet. You just want to try somet
162
162
 
163
163
  **Sandboxes** are temporary, local-only worktrees designed for this:
164
164
 
165
- 1. **Create**: `yggtree wt create-sandbox` (creates `branch_qes2`).
165
+ 1. **Create**: `yggtree wt create-sandbox` (creates something like `sandbox-a3f2_feature-branch`).
166
166
  2. **Experiment**: Change files, run tests, try that risky refactor.
167
167
  3. **Apply**: `yggtree wt apply` to "push" those file changes back to your origin directory.
168
168
  4. **Unapply**: Don't like it? `yggtree wt unapply` restores your origin to exactly how it was before.
@@ -177,10 +177,13 @@ Yggdrasil automatically prepares each worktree.
177
177
 
178
178
  Resolution order:
179
179
 
180
- 1. `yggtree-worktree.json` inside the worktree
181
- 2. `yggtree-worktree.json` in the repo root
182
- 3. `.cursor/worktrees.json`
183
- 4. Fallback: `npm install` + submodules
180
+ 1. `.yggtree/worktree-setup.json` in the repo root
181
+ 2. `yggtree-worktree.json` in the repo root (legacy fallback)
182
+ 3. `.cursor/worktrees.json` in the repo root (legacy fallback)
183
+ 4. `.yggtree/worktree-setup.json` inside the worktree (per-worktree fallback)
184
+ 5. `yggtree-worktree.json` inside the worktree (legacy fallback)
185
+ 6. `.cursor/worktrees.json` inside the worktree (legacy fallback)
186
+ 7. Fallback: `npm install` + submodules
184
187
 
185
188
  ### Example configuration
186
189
 
@@ -211,12 +214,18 @@ Create a worktree from a branch.
211
214
 
212
215
  Options:
213
216
 
217
+ * `-b, --branch <name>`
214
218
  * `--base <ref>`
215
219
  * `--source local|remote`
216
220
  * `--no-bootstrap`
217
221
  * `--enter / --no-enter`
218
222
  * `--exec "<command>"`
219
223
 
224
+ Interactive flow:
225
+
226
+ * Instead of asking for a free-form `exec` command, yggtree now asks if you want to open a tool after creation (IDE or agent CLI).
227
+ * `--exec` remains available as an advanced explicit override.
228
+
220
229
  <details>
221
230
  <summary>Example</summary>
222
231
 
@@ -228,17 +237,59 @@ yggtree wt create feat/new-ui --base main --exec "cursor ."
228
237
 
229
238
  ---
230
239
 
240
+ ### `yggtree wt worktree-checkout [name] [ref]`
241
+
242
+ Create a checkout-style worktree from an existing branch.
243
+
244
+ Behavior:
245
+
246
+ * Prompts a searchable branch picker (type to filter in real time).
247
+ * Attaches the new worktree directly to the selected branch (checkout-style).
248
+ * If you select a remote-only branch (`origin/*`), yggtree creates the local branch in the new worktree automatically.
249
+ * If that branch already has an active yggtree-managed worktree, yggtree falls back to entering that worktree instead of creating a duplicate.
250
+
251
+ Options:
252
+
253
+ * `-n, --name <slug>`
254
+ * `-r, --ref <ref>`: skip picker and use a specific branch (`feature/x` or `origin/feature/x`)
255
+ * `--no-bootstrap`
256
+ * `--enter / --no-enter`
257
+ * `--exec "<command>"`
258
+
259
+ Interactive flow:
260
+
261
+ * Instead of asking for a free-form `exec` command, yggtree now asks if you want to open a tool after creation (IDE or agent CLI).
262
+ * `--exec` remains available as an advanced explicit override.
263
+
264
+ <details>
265
+ <summary>Example</summary>
266
+
267
+ ```bash
268
+ yggtree wt worktree-checkout -n hotfix-auth -r main --no-enter
269
+ ```
270
+
271
+ </details>
272
+
273
+ ---
274
+
231
275
  ### `yggtree wt create-sandbox`
232
276
 
233
277
  Create a temporary sandbox from your current local branch.
234
278
 
235
279
  Options:
236
280
 
281
+ * `-n, --name <name>`: Optional sandbox name (auto-generated if omitted).
237
282
  * `--carry / --no-carry`: Bring uncommitted changes (staged/unstaged/untracked) with you.
238
283
  * `--no-bootstrap`
239
284
  * `--enter / --no-enter`
240
285
  * `--exec "<command>"`
241
286
 
287
+ Interactive flow:
288
+
289
+ * Prompts for an optional sandbox name (leave empty to auto-generate one from current branch).
290
+ * Instead of asking for a free-form `exec` command, yggtree now asks if you want to open a tool after creation (IDE or agent CLI).
291
+ * `--exec` remains available as an advanced explicit override.
292
+
242
293
  ---
243
294
 
244
295
  ### `yggtree wt apply`
@@ -261,6 +312,12 @@ Undo a previous `apply` operation.
261
312
 
262
313
  Create multiple worktrees at once.
263
314
 
315
+ Options:
316
+
317
+ * `--base <ref>`
318
+ * `--source local|remote`
319
+ * `--no-bootstrap`
320
+
264
321
  <details>
265
322
  <summary>Example</summary>
266
323
 
@@ -274,14 +331,21 @@ yggtree wt create-multi --base main
274
331
 
275
332
  ### `yggtree wt list`
276
333
 
277
- List all worktrees with state.
334
+ List all repo-linked worktrees with state.
278
335
 
279
336
  Columns:
280
337
 
281
- * TYPE (MAIN / MANAGED)
338
+ * TYPE (`MAIN`, `MANAGED`, `LINKED`, `SANDBOX`)
282
339
  * STATE (clean / dirty)
340
+ * LAST ACTIVE
283
341
  * BRANCH
284
- * PATH
342
+
343
+ Notes:
344
+
345
+ * Entries are grouped by `TYPE`.
346
+ * `SANDBOX` and `MANAGED` are worktrees inside `~/.yggtree`.
347
+ * External worktrees are labeled `LINKED`.
348
+ * Use `--open` to switch this flow into "pick and open in tool" mode.
285
349
 
286
350
  ---
287
351
 
@@ -307,6 +371,35 @@ yggtree wt enter feat/new-ui --exec "npm test"
307
371
 
308
372
  ---
309
373
 
374
+ ### `yggtree wt open [worktree]`
375
+
376
+ Open a worktree in an IDE or agent CLI.
377
+
378
+ Behavior:
379
+
380
+ * If `[worktree]` is omitted, you can pick from the worktree list with type-to-filter search.
381
+ * Detects available tool commands in your `PATH` (for example: IDEs like `cursor`, `code`, `zed`; agents like `claude`, `codex`, `gemini`, `opencode`).
382
+ * Lets you choose one interactively, or pass `--tool`.
383
+ * If an agent CLI is selected, yggtree opens a sub-shell and launches it there.
384
+
385
+ Options:
386
+
387
+ * `--tool <command>`
388
+
389
+ <details>
390
+ <summary>Examples</summary>
391
+
392
+ ```bash
393
+ yggtree wt open
394
+ yggtree wt open feat/new-ui --tool cursor
395
+ yggtree wt open feat/new-ui --tool claude
396
+ yggtree wt list --open
397
+ ```
398
+
399
+ </details>
400
+
401
+ ---
402
+
310
403
  ### `yggtree wt exec [worktree] -- <command>`
311
404
 
312
405
  Run a command inside a worktree **without entering**.
@@ -338,7 +431,18 @@ Re‑run bootstrap commands for a worktree.
338
431
 
339
432
  ### `yggtree wt delete`
340
433
 
341
- Interactively delete managed worktrees.
434
+ Interactively delete worktrees.
435
+
436
+ Behavior:
437
+
438
+ * Default flow targets managed worktrees.
439
+ * In interactive mode, yggtree asks whether to include external linked worktrees.
440
+ * In direct CLI usage, `--all` includes external linked worktrees (main/current are still excluded for safety).
441
+ * The delete selector shows 6 items per page.
442
+
443
+ Optional:
444
+
445
+ * `--all` includes linked worktrees outside `~/.yggtree` (main/current worktree is excluded for safety)
342
446
 
343
447
  ---
344
448
 
@@ -488,7 +592,7 @@ yggtree wt create-sandbox --carry
488
592
  **Scenario:**
489
593
 
490
594
  1. You have 5 files changed in your main repo but aren't sure about the direction.
491
- 2. Run `create-sandbox --carry` to move those changes into an isolated `current-branch_a1b2` folder.
595
+ 2. Run `create-sandbox --carry` to move those changes into an isolated `sandbox-a3f2_feature-branch` folder.
492
596
  3. Experiment freely.
493
597
  4. If it works: `yggtree wt apply`.
494
598
  5. If it fails: Just delete the sandbox or `unapply`.
@@ -0,0 +1,187 @@
1
+ import chalk from 'chalk';
2
+ import gradient from 'gradient-string';
3
+ import figlet from 'figlet';
4
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5
+ export async function bifrostCommand() {
6
+ console.clear();
7
+ process.stdout.write('\x1b[?25l'); // Hide cursor
8
+ // Animated clouds blowing across the sky
9
+ const cloudArt = ' ☁️ ☁️ ☁️ ';
10
+ const skyWidth = 40;
11
+ console.log(chalk.dim('\n The sky darkens...\n'));
12
+ // Print a placeholder for the animated line so we can cursor-up over it
13
+ console.log('');
14
+ for (let i = 0; i < 20; i++) {
15
+ // Shift the clouds by slicing from an ever-changing offset of a repeated string
16
+ const offset = i % cloudArt.length;
17
+ const visibleClouds = (cloudArt.repeat(3)).substring(offset, offset + skyWidth);
18
+ process.stdout.write('\x1b[1A'); // Move cursor up 1 line
19
+ process.stdout.write('\r ' + chalk.gray(visibleClouds) + '\n');
20
+ await sleep(100);
21
+ }
22
+ console.clear();
23
+ // 6-Frame Cutscene: The Bifrost Opening (Bigger & Stronger)
24
+ const bifrostFrames = [
25
+ // Frame 1: Empty sky
26
+ chalk.gray(`
27
+ ☁️ ☁️
28
+
29
+
30
+
31
+
32
+
33
+
34
+
35
+
36
+
37
+
38
+ `),
39
+ // Frame 2: Small beam of light
40
+ chalk.cyan(`
41
+ ☁️ | ☁️
42
+ |
43
+ |
44
+ |
45
+ |
46
+ |
47
+ |
48
+ |
49
+ |
50
+ |
51
+ v
52
+ `),
53
+ // Frame 3: Wider beam reaching the ground
54
+ gradient.rainbow(`
55
+ ☁️ ||| ☁️
56
+ |||
57
+ |||
58
+ |||
59
+ |||
60
+ |||
61
+ |||
62
+ |||
63
+ |||
64
+ |||
65
+ ---------
66
+ `),
67
+ // Frame 4: Giant beam hitting the ground
68
+ gradient.rainbow(`
69
+ ☁️ ||||||||||| ☁️
70
+ |||||||||||
71
+ |||||||||||
72
+ |||||||||||
73
+ |||||||||||
74
+ |||||||||||
75
+ |||||||||||
76
+ |||||||||||
77
+ |||||||||||
78
+ |||||||||||
79
+ -----------------
80
+ `),
81
+ // Frame 5: MASSIVE beam
82
+ gradient.rainbow(`
83
+ ☁️ ||||||||||||||||||||| ☁️
84
+ |||||||||||||||||||||
85
+ |||||||||||||||||||||
86
+ |||||||||||||||||||||
87
+ |||||||||||||||||||||
88
+ |||||||||||||||||||||
89
+ |||||||||||||||||||||
90
+ |||||||||||||||||||||
91
+ |||||||||||||||||||||
92
+ |||||||||||||||||||||
93
+ ---------------------------
94
+ `),
95
+ // Frame 6: Silhouette of Thor in the massive beam
96
+ gradient.rainbow(`
97
+ ☁️ ||||||||||||||||||||| ☁️
98
+ |||||||||||||||||||||
99
+ |||||||||||||||||||||
100
+ |||||||||||||||||||||
101
+ |||||||||||||||||||||
102
+ |||||||||||||||||||||
103
+ |||||||||||||||||||||
104
+ |||||||||||||||||||||
105
+ |||||||||||||||||||||
106
+ ||||||||| 𖨆 |||||||||
107
+ ---------------------------
108
+ `)
109
+ ];
110
+ // Give the user more time to digest each step of the expanding Bifrost
111
+ const frameDelays = [400, 300, 300, 300, 400, 1000];
112
+ for (let i = 0; i < bifrostFrames.length; i++) {
113
+ console.clear();
114
+ console.log(chalk.bold.italic('\n Opening the Bifrost...\n'));
115
+ console.log(bifrostFrames[i]);
116
+ await sleep(frameDelays[i]); // Custom timing per frame for better impact
117
+ }
118
+ await sleep(200);
119
+ // Lightning Flash
120
+ // We use standard ANSI terminal invert colors to simulate lightning
121
+ process.stdout.write('\x1b[?5h'); // Inverse Video On
122
+ console.log(chalk.yellow.bold('\n\n ⚡ \n ⚡ ⚡\n ⚡\n\n'));
123
+ await sleep(150);
124
+ process.stdout.write('\x1b[?5l'); // Inverse Video Off
125
+ console.clear();
126
+ const frames = [
127
+ chalk.gray.bold(`
128
+ ___________
129
+ | |
130
+ | MJÖLNIR |
131
+ |___________|
132
+ | |
133
+ | |
134
+ _|_|_
135
+ `),
136
+ chalk.gray.bold(`
137
+
138
+ _____________
139
+ ======| MJÖLNIR |
140
+ -------------
141
+
142
+ `),
143
+ chalk.gray.bold(`
144
+ _|_|_
145
+ | |
146
+ | |
147
+ ___________
148
+ | |
149
+ | MJÖLNIR |
150
+ |___________|
151
+ `),
152
+ chalk.gray.bold(`
153
+
154
+ _____________
155
+ | MJÖLNIR |======
156
+ -------------
157
+
158
+ `)
159
+ ];
160
+ // Let the hammer fall down and spin from right to left
161
+ const animationSteps = 15;
162
+ for (let i = 0; i < animationSteps; i++) {
163
+ console.clear();
164
+ console.log('\x1b[?25l'); // Hide cursor
165
+ const yPadding = '\n'.repeat(i);
166
+ const xPaddingLength = Math.max(0, (animationSteps - i) * 6);
167
+ const xPadding = ' '.repeat(xPaddingLength);
168
+ const frame = frames[i % frames.length];
169
+ const paddedFrame = frame.split('\n').map(line => xPadding + line).join('\n');
170
+ console.log(yPadding + paddedFrame);
171
+ await sleep(70);
172
+ }
173
+ // Final pose upright!
174
+ console.clear();
175
+ const finalYPadding = '\n'.repeat(animationSteps);
176
+ console.log(finalYPadding + frames[0]);
177
+ await sleep(200);
178
+ // THOOM text
179
+ const thoomArt = figlet.textSync('THOOM!', {
180
+ font: 'Standard',
181
+ horizontalLayout: 'default',
182
+ verticalLayout: 'default',
183
+ });
184
+ console.log(gradient.fruit(thoomArt));
185
+ console.log(chalk.yellow.bold('\n ⚡ The son of Odin has entered the realm ⚡ \n'));
186
+ console.log('\x1b[?25h'); // Show cursor
187
+ }
@@ -0,0 +1,39 @@
1
+ import chalk from 'chalk';
2
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3
+ // A collection of legendary quotes (mix of Poetic Edda and developer humor)
4
+ const quotes = [
5
+ "I say thee NAY to unhandled promises!",
6
+ "Even the All-Father tests in production sometimes.",
7
+ "A wise developer commits early, and commits often.",
8
+ "Bugs are just frost giants hiding in your codebase.",
9
+ "May your builds be as swift as Mjölnir's flight.",
10
+ "Do not trust a tree without deep roots, nor code without tests.",
11
+ "Yggdrasil connects all realms; git connects all branches.",
12
+ "A clean working tree is acceptable to the gods.",
13
+ "The Bifrost is open, but did you remember to push?",
14
+ "Verily, thou shalt not force push to main."
15
+ ];
16
+ export async function thorCommand() {
17
+ console.clear();
18
+ const randomQuote = quotes[Math.floor(Math.random() * quotes.length)];
19
+ // Static Thor ASCII art
20
+ const thorArt = `
21
+ / \\
22
+ /___\\
23
+ (o_o) "Hello from Asgard!"
24
+ /|||||\\
25
+ | | | |
26
+ \\=====//
27
+ | |
28
+ | |
29
+ _|_|_
30
+ `;
31
+ console.log(chalk.gray.bold(thorArt));
32
+ // Typewriter effect for the quote
33
+ process.stdout.write(chalk.cyan('Thor says: '));
34
+ for (let i = 0; i < randomQuote.length; i++) {
35
+ process.stdout.write(chalk.bold.yellow(randomQuote.charAt(i)));
36
+ await sleep(30); // Typing speed
37
+ }
38
+ console.log('\n');
39
+ }
@@ -5,6 +5,7 @@ import { getRepoRoot, getRepoName, verifyRef, fetchAll, getCurrentBranch, ensure
5
5
  import { runBootstrap } from '../../lib/config.js';
6
6
  import { WORKTREES_ROOT } from '../../lib/paths.js';
7
7
  import { log, ui, createSpinner } from '../../lib/ui.js';
8
+ import { buildAgentExecCommand, detectInstalledOpenTools, isAgentTool, launchOpenTool, promptOpenToolSelection, } from './open.js';
8
9
  import { execa } from 'execa';
9
10
  import { spawn } from 'child_process';
10
11
  import fs from 'fs-extra';
@@ -58,19 +59,29 @@ export async function createCommandNew(options) {
58
59
  when: options.enter === undefined,
59
60
  },
60
61
  {
61
- type: 'input',
62
- name: 'exec',
63
- message: 'Command to run after creation (optional):',
64
- default: options.exec,
62
+ type: 'confirm',
63
+ name: 'shouldOpenTool',
64
+ message: 'Open a tool after creation? (IDE or agent CLI)',
65
+ default: false,
65
66
  when: options.exec === undefined,
66
- }
67
+ },
67
68
  ]);
68
69
  const branchName = options.branch || answers.branch;
69
70
  let baseRef = options.base || answers.base;
70
71
  const source = options.source || answers.source;
71
72
  const shouldEnter = options.enter !== undefined ? options.enter : answers.shouldEnter;
72
73
  const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
73
- const execCommandStr = options.exec || answers.exec;
74
+ let selectedTool;
75
+ if (options.exec === undefined && answers.shouldOpenTool) {
76
+ const installedTools = await detectInstalledOpenTools();
77
+ if (installedTools.length === 0) {
78
+ log.warning('No IDE/agent tool detected in PATH. Skipping open step.');
79
+ }
80
+ else {
81
+ selectedTool = await promptOpenToolSelection(installedTools, 'Select tool to open:');
82
+ }
83
+ }
84
+ const execCommandStr = options.exec || (selectedTool && isAgentTool(selectedTool) ? buildAgentExecCommand(selectedTool) : undefined);
74
85
  // Append origin/ if remote is selected and not already present
75
86
  if (!options.base && source === 'remote' && !baseRef.startsWith('origin/')) {
76
87
  baseRef = `origin/${baseRef}`;
@@ -159,6 +170,15 @@ export async function createCommandNew(options) {
159
170
  ]);
160
171
  }
161
172
  }
173
+ if (selectedTool && !isAgentTool(selectedTool)) {
174
+ try {
175
+ log.info(`Opening ${ui.path(wtPath)} in ${chalk.cyan(selectedTool.name)}...`);
176
+ await launchOpenTool(selectedTool, wtPath);
177
+ }
178
+ catch (error) {
179
+ log.warning(`Could not open ${selectedTool.name}: ${error.message}`);
180
+ }
181
+ }
162
182
  // 6. Final Output
163
183
  log.success('Worktree ready!');
164
184
  if (shouldEnter) {
@@ -5,7 +5,8 @@ import { getRepoRoot, getRepoName, getCurrentBranch } from '../../lib/git.js';
5
5
  import { runBootstrap } from '../../lib/config.js';
6
6
  import { WORKTREES_ROOT } from '../../lib/paths.js';
7
7
  import { log, ui, createSpinner } from '../../lib/ui.js';
8
- import { generateSandboxName, writeSandboxMeta } from '../../lib/sandbox.js';
8
+ import { generateSandboxName, normalizeSandboxName, writeSandboxMeta } from '../../lib/sandbox.js';
9
+ import { buildAgentExecCommand, detectInstalledOpenTools, isAgentTool, launchOpenTool, promptOpenToolSelection, } from './open.js';
9
10
  import { execa } from 'execa';
10
11
  import { spawn } from 'child_process';
11
12
  import fs from 'fs-extra';
@@ -20,11 +21,32 @@ export async function createSandboxCommand(options = {}) {
20
21
  return;
21
22
  }
22
23
  log.info(`Current branch: ${chalk.cyan(currentBranch)}`);
23
- // 2. Generate random sandbox name
24
- const sandboxName = generateSandboxName(currentBranch);
25
- log.info(`Sandbox name: ${chalk.yellow(sandboxName)}`);
24
+ // 2. Build default + optional custom name validation
25
+ const generatedSandboxName = generateSandboxName(currentBranch);
26
+ const validateSandboxName = async (input) => {
27
+ if (!input.trim())
28
+ return true;
29
+ const normalized = normalizeSandboxName(input);
30
+ if (!normalized) {
31
+ return 'Please provide at least one valid character.';
32
+ }
33
+ try {
34
+ await execa('git', ['check-ref-format', '--branch', normalized], { cwd: repoRoot });
35
+ return true;
36
+ }
37
+ catch {
38
+ return 'Sandbox name is not a valid git branch name.';
39
+ }
40
+ };
26
41
  // 3. Gather remaining inputs
27
42
  const answers = await inquirer.prompt([
43
+ {
44
+ type: 'input',
45
+ name: 'name',
46
+ message: `Sandbox name (optional, leave empty for auto: ${generatedSandboxName}):`,
47
+ when: options.name === undefined,
48
+ validate: validateSandboxName,
49
+ },
28
50
  {
29
51
  type: 'confirm',
30
52
  name: 'carry',
@@ -47,17 +69,39 @@ export async function createSandboxCommand(options = {}) {
47
69
  when: options.enter === undefined,
48
70
  },
49
71
  {
50
- type: 'input',
51
- name: 'exec',
52
- message: 'Command to run after creation (optional):',
53
- default: options.exec,
72
+ type: 'confirm',
73
+ name: 'shouldOpenTool',
74
+ message: 'Open a tool after creation? (IDE or agent CLI)',
75
+ default: false,
54
76
  when: options.exec === undefined,
55
- }
77
+ },
56
78
  ]);
79
+ const requestedSandboxName = options.name !== undefined ? options.name : answers.name || '';
80
+ const nameValidation = await validateSandboxName(requestedSandboxName);
81
+ if (nameValidation !== true) {
82
+ log.error(nameValidation);
83
+ return;
84
+ }
85
+ const normalizedCustomName = normalizeSandboxName(requestedSandboxName);
86
+ const sandboxName = normalizedCustomName || generatedSandboxName;
87
+ if (requestedSandboxName.trim() && normalizedCustomName !== requestedSandboxName.trim()) {
88
+ log.dim(`Normalized sandbox name: ${chalk.yellow(sandboxName)}`);
89
+ }
90
+ log.info(`Sandbox name: ${chalk.yellow(sandboxName)}`);
57
91
  const shouldCarry = options.carry !== undefined ? options.carry : answers.carry;
58
92
  const shouldEnter = options.enter !== undefined ? options.enter : answers.shouldEnter;
59
93
  const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
60
- const execCommandStr = options.exec || answers.exec;
94
+ let selectedTool;
95
+ if (options.exec === undefined && answers.shouldOpenTool) {
96
+ const installedTools = await detectInstalledOpenTools();
97
+ if (installedTools.length === 0) {
98
+ log.warning('No IDE/agent tool detected in PATH. Skipping open step.');
99
+ }
100
+ else {
101
+ selectedTool = await promptOpenToolSelection(installedTools, 'Select tool to open:');
102
+ }
103
+ }
104
+ const execCommandStr = options.exec || (selectedTool && isAgentTool(selectedTool) ? buildAgentExecCommand(selectedTool) : undefined);
61
105
  // 4. Detect uncommitted changes before creating worktree
62
106
  let changedFiles = [];
63
107
  let submodulePaths = [];
@@ -159,6 +203,15 @@ export async function createSandboxCommand(options = {}) {
159
203
  ]);
160
204
  }
161
205
  }
206
+ if (selectedTool && !isAgentTool(selectedTool)) {
207
+ try {
208
+ log.info(`Opening ${ui.path(wtPath)} in ${chalk.cyan(selectedTool.name)}...`);
209
+ await launchOpenTool(selectedTool, wtPath);
210
+ }
211
+ catch (error) {
212
+ log.warning(`Could not open ${selectedTool.name}: ${error.message}`);
213
+ }
214
+ }
162
215
  // 10. Final output
163
216
  log.success('Sandbox ready!');
164
217
  log.info(`Use ${chalk.cyan('yggtree wt apply')} to apply changes to origin.`);