synabun 2026.3.31007 → 2026.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +51 -52
  2. package/hooks/claude-code/prompt-submit.mjs +6 -6
  3. package/mcp-server/dist/services/neural-interface.d.ts +20 -19
  4. package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
  5. package/mcp-server/dist/services/neural-interface.js +52 -45
  6. package/mcp-server/dist/services/neural-interface.js.map +1 -1
  7. package/mcp-server/dist/tools/browser-advanced.d.ts +4 -0
  8. package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -1
  9. package/mcp-server/dist/tools/browser-advanced.js +7 -4
  10. package/mcp-server/dist/tools/browser-advanced.js.map +1 -1
  11. package/mcp-server/dist/tools/browser-interact.d.ts +16 -0
  12. package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -1
  13. package/mcp-server/dist/tools/browser-interact.js +25 -16
  14. package/mcp-server/dist/tools/browser-interact.js.map +1 -1
  15. package/mcp-server/dist/tools/browser-navigate.d.ts +8 -0
  16. package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -1
  17. package/mcp-server/dist/tools/browser-navigate.js +13 -8
  18. package/mcp-server/dist/tools/browser-navigate.js.map +1 -1
  19. package/mcp-server/dist/tools/browser-observe.d.ts +48 -0
  20. package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -1
  21. package/mcp-server/dist/tools/browser-observe.js +74 -49
  22. package/mcp-server/dist/tools/browser-observe.js.map +1 -1
  23. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +20 -0
  24. package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -1
  25. package/mcp-server/dist/tools/leonardo-browser-tools.js +39 -34
  26. package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -1
  27. package/mcp-server/src/services/neural-interface.ts +58 -46
  28. package/mcp-server/src/tools/browser-advanced.ts +10 -5
  29. package/mcp-server/src/tools/browser-interact.ts +34 -24
  30. package/mcp-server/src/tools/browser-navigate.ts +83 -77
  31. package/mcp-server/src/tools/browser-observe.ts +99 -73
  32. package/mcp-server/src/tools/leonardo-browser-tools.ts +47 -37
  33. package/neural-interface/public/shared/styles.css +49 -0
  34. package/neural-interface/public/shared/ui-automation-studio.js +71 -3
  35. package/neural-interface/public/shared/ui-claude-panel.js +29 -23
  36. package/neural-interface/public/shared/ui-settings.js +10 -10
  37. package/neural-interface/public/shared/ui-terminal.js +9 -7
  38. package/neural-interface/server.js +732 -386
  39. package/package.json +1 -1
@@ -10,15 +10,11 @@ import { z } from 'zod';
10
10
  import * as ni from '../services/neural-interface.js';
11
11
  import { text } from './response.js';
12
12
 
13
- const LEO_BASE = 'https://app.leonardo.ai';
13
+ const tabIdField = z.string().optional().describe('Target a specific tab within the session. Auto-resolved from environment if omitted.');
14
14
 
15
- // ── Helper: resolve or create session ─────────────────────────
15
+ const LEO_BASE = 'https://app.leonardo.ai';
16
16
 
17
- async function getSession(): Promise<string | null> {
18
- const resolved = await ni.resolveSession(undefined, { url: LEO_BASE });
19
- if ('error' in resolved) return null;
20
- return resolved.sessionId;
21
- }
17
+ // ── Helper ────────────────────────────────────────────────────
22
18
 
23
19
  async function wait(ms: number): Promise<void> {
24
20
  return new Promise(resolve => setTimeout(resolve, ms));
@@ -31,14 +27,16 @@ export const browserNavigateSchema = {
31
27
  'home', 'library', 'image', 'video', 'upscaler',
32
28
  'blueprints', 'flow-state', 'models',
33
29
  ] as const).describe('Leonardo.ai page to navigate to.'),
30
+ sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
31
+ tabId: tabIdField,
34
32
  };
35
33
 
36
34
  export const browserNavigateDescription =
37
35
  'Navigate the browser to a specific Leonardo.ai page. Always use this as the first step before any browser-based generation.';
38
36
 
39
- export async function handleBrowserNavigate(args: { page: string }) {
40
- const sessionId = await getSession();
41
- if (!sessionId) return text('No browser session available. Open Leonardo.ai in the browser first using browser_navigate or the Apps menu.');
37
+ export async function handleBrowserNavigate(args: { page: string; sessionId?: string; tabId?: string }) {
38
+ const resolved = await ni.resolveSession(args.sessionId, { url: LEO_BASE }, args.tabId);
39
+ if ('error' in resolved) return text('No browser session available. Open Leonardo.ai in the browser first using browser_navigate or the Apps menu.');
42
40
 
43
41
  const paths: Record<string, string> = {
44
42
  'home': '/',
@@ -52,7 +50,7 @@ export async function handleBrowserNavigate(args: { page: string }) {
52
50
  };
53
51
 
54
52
  const path = paths[args.page] || '/';
55
- const res = await ni.navigate(sessionId, `${LEO_BASE}${path}`);
53
+ const res = await ni.navigate(resolved.sessionId, `${LEO_BASE}${path}`, resolved.tabId);
56
54
  if (res.error) return text(`Navigation failed: ${res.error}`);
57
55
 
58
56
  // Wait for page to settle
@@ -66,6 +64,8 @@ export async function handleBrowserNavigate(args: { page: string }) {
66
64
  export const browserGenerateSchema = {
67
65
  prompt: z.string().describe('The generation prompt to type into Leonardo.ai.'),
68
66
  type: z.enum(['image', 'video'] as const).optional().describe('Generation type. Default: image.'),
67
+ sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
68
+ tabId: tabIdField,
69
69
  };
70
70
 
71
71
  export const browserGenerateDescription =
@@ -74,31 +74,33 @@ export const browserGenerateDescription =
74
74
  export async function handleBrowserGenerate(args: {
75
75
  prompt: string;
76
76
  type?: string;
77
+ sessionId?: string;
78
+ tabId?: string;
77
79
  }) {
78
- const sessionId = await getSession();
79
- if (!sessionId) return text('No browser session available. Open Leonardo.ai first.');
80
+ const resolved = await ni.resolveSession(args.sessionId, { url: LEO_BASE }, args.tabId);
81
+ if ('error' in resolved) return text('No browser session available. Open Leonardo.ai first.');
80
82
 
81
83
  const genType = args.type || 'image';
82
84
 
83
85
  // Check current URL — navigate only if not already on the right page
84
- const snapshot = await ni.snapshot(sessionId);
86
+ const snapshot = await ni.snapshot(resolved.sessionId, resolved.tabId);
85
87
  const currentUrl = (snapshot as any)?.url || '';
86
88
  const targetPath = genType === 'video' ? '/image-generation/video' : '/image-generation';
87
89
 
88
90
  if (!currentUrl.includes(targetPath)) {
89
- const navRes = await ni.navigate(sessionId, `${LEO_BASE}${targetPath}`);
91
+ const navRes = await ni.navigate(resolved.sessionId, `${LEO_BASE}${targetPath}`, resolved.tabId);
90
92
  if (navRes.error) return text(`Navigation failed: ${navRes.error}`);
91
93
  await wait(3000);
92
94
  }
93
95
 
94
96
  // Clear and fill the prompt textbox
95
- const fillRes = await ni.fill(sessionId, '[aria-label="Prompt"], textbox[name="Prompt"], textarea', args.prompt);
97
+ const fillRes = await ni.fill(resolved.sessionId, '[aria-label="Prompt"], textbox[name="Prompt"], textarea', args.prompt, undefined, resolved.tabId);
96
98
  if (fillRes.error) return text(`Failed to fill prompt: ${fillRes.error}`);
97
99
 
98
100
  await wait(500);
99
101
 
100
102
  // Click the Generate button
101
- const clickRes = await ni.click(sessionId, 'button[aria-label="Generate"], button:has-text("Generate")');
103
+ const clickRes = await ni.click(resolved.sessionId, 'button[aria-label="Generate"], button:has-text("Generate")', undefined, resolved.tabId);
102
104
  if (clickRes.error) return text(`Failed to click Generate: ${clickRes.error}`);
103
105
 
104
106
  return text(`Generation started (${genType}).\n\nPrompt: "${args.prompt.substring(0, 120)}${args.prompt.length > 120 ? '...' : ''}"\n\nThe generation is processing in Leonardo.ai. Use browser_snapshot or browser_screenshot to check progress and see results. Results also appear in the library.`);
@@ -109,24 +111,26 @@ export async function handleBrowserGenerate(args: {
109
111
  export const browserLibrarySchema = {
110
112
  action: z.enum(['view', 'search'] as const).describe('Action: view (open library) or search (search for a term).'),
111
113
  query: z.string().optional().describe('Search query (for search action).'),
114
+ sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
115
+ tabId: tabIdField,
112
116
  };
113
117
 
114
118
  export const browserLibraryDescription =
115
119
  'Open or search Leonardo.ai\'s library to view past generations. Use browser_snapshot after to see results.';
116
120
 
117
- export async function handleBrowserLibrary(args: { action: string; query?: string }) {
118
- const sessionId = await getSession();
119
- if (!sessionId) return text('No browser session available.');
121
+ export async function handleBrowserLibrary(args: { action: string; query?: string; sessionId?: string; tabId?: string }) {
122
+ const resolved = await ni.resolveSession(args.sessionId, { url: LEO_BASE }, args.tabId);
123
+ if ('error' in resolved) return text('No browser session available.');
120
124
 
121
- const navRes = await ni.navigate(sessionId, `${LEO_BASE}/library`);
125
+ const navRes = await ni.navigate(resolved.sessionId, `${LEO_BASE}/library`, resolved.tabId);
122
126
  if (navRes.error) return text(`Navigation failed: ${navRes.error}`);
123
127
 
124
128
  await wait(2000);
125
129
 
126
130
  if (args.action === 'search' && args.query) {
127
- const fillRes = await ni.fill(sessionId, 'input[placeholder*="Search" i]', args.query);
131
+ const fillRes = await ni.fill(resolved.sessionId, 'input[placeholder*="Search" i]', args.query, undefined, resolved.tabId);
128
132
  if (fillRes.error) return text(`Failed to search: ${fillRes.error}`);
129
- await ni.pressKey(sessionId, 'Enter');
133
+ await ni.pressKey(resolved.sessionId, 'Enter', resolved.tabId);
130
134
  return text(`Searched Leonardo.ai library for: "${args.query}". Use browser_snapshot to see results.`);
131
135
  }
132
136
 
@@ -137,17 +141,19 @@ export async function handleBrowserLibrary(args: { action: string; query?: strin
137
141
 
138
142
  export const browserDownloadSchema = {
139
143
  action: z.enum(['screenshot'] as const).describe('Action: screenshot (capture current Leonardo.ai page state).'),
144
+ sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
145
+ tabId: tabIdField,
140
146
  };
141
147
 
142
148
  export const browserDownloadDescription =
143
149
  'Capture a screenshot of the current Leonardo.ai page. Use to verify generation results or UI state.';
144
150
 
145
- export async function handleBrowserDownload(args: { action: string }) {
146
- const sessionId = await getSession();
147
- if (!sessionId) return text('No browser session available.');
151
+ export async function handleBrowserDownload(args: { action: string; sessionId?: string; tabId?: string }) {
152
+ const resolved = await ni.resolveSession(args.sessionId, { url: LEO_BASE }, args.tabId);
153
+ if ('error' in resolved) return text('No browser session available.');
148
154
 
149
155
  if (args.action === 'screenshot') {
150
- const res = await ni.screenshot(sessionId);
156
+ const res = await ni.screenshot(resolved.sessionId, resolved.tabId);
151
157
  if (res.error) return text(`Screenshot failed: ${res.error}`);
152
158
  return text('Screenshot captured of the current Leonardo.ai page.');
153
159
  }
@@ -171,6 +177,8 @@ export const browserReferenceSchema = {
171
177
  autoClear: z.boolean().optional().describe(
172
178
  'Auto-delete the source images from data/images/ after successful upload. Default: true.',
173
179
  ),
180
+ sessionId: z.string().optional().describe('Browser session ID. If omitted, auto-selects the only open session.'),
181
+ tabId: tabIdField,
174
182
  };
175
183
 
176
184
  export const browserReferenceDescription =
@@ -183,9 +191,11 @@ export async function handleBrowserReference(args: {
183
191
  type: string;
184
192
  filePaths: string[];
185
193
  autoClear?: boolean;
194
+ sessionId?: string;
195
+ tabId?: string;
186
196
  }) {
187
- const sessionId = await getSession();
188
- if (!sessionId) return text('No browser session available. Open Leonardo.ai first.');
197
+ const resolved = await ni.resolveSession(args.sessionId, { url: LEO_BASE }, args.tabId);
198
+ if ('error' in resolved) return text('No browser session available. Open Leonardo.ai first.');
189
199
 
190
200
  const { type, filePaths, autoClear = true } = args;
191
201
  const isVideo = type === 'start_frame' || type === 'end_frame';
@@ -195,7 +205,7 @@ export async function handleBrowserReference(args: {
195
205
 
196
206
  if (isVideo) {
197
207
  // Video: click "Add Image Guidance to generation" button
198
- openResult = await ni.evaluate(sessionId, `(() => {
208
+ openResult = await ni.evaluate(resolved.sessionId, `(() => {
199
209
  const btns = document.querySelectorAll('button');
200
210
  for (const b of btns) {
201
211
  if (b.textContent.includes('Image Guidance') || b.textContent.includes('Add Image')) {
@@ -203,10 +213,10 @@ export async function handleBrowserReference(args: {
203
213
  }
204
214
  }
205
215
  return 'not found';
206
- })()`);
216
+ })()`, resolved.tabId);
207
217
  } else {
208
218
  // Image: click "Add elements" button
209
- openResult = await ni.evaluate(sessionId, `(() => {
219
+ openResult = await ni.evaluate(resolved.sessionId, `(() => {
210
220
  const btns = document.querySelectorAll('button');
211
221
  for (const b of btns) {
212
222
  if (b.textContent.includes('Add elements') || b.textContent.includes('Add Elements')) {
@@ -214,7 +224,7 @@ export async function handleBrowserReference(args: {
214
224
  }
215
225
  }
216
226
  return 'not found';
217
- })()`);
227
+ })()`, resolved.tabId);
218
228
  }
219
229
 
220
230
  if (openResult.error) return text(`Failed to open reference panel: ${openResult.error}`);
@@ -233,23 +243,23 @@ export async function handleBrowserReference(args: {
233
243
  const labels = typeLabels[type] || [type];
234
244
  const labelSearch = labels.map(l => `b.textContent.includes('${l}')`).join(' || ');
235
245
 
236
- const selectResult = await ni.evaluate(sessionId, `(() => {
246
+ const selectResult = await ni.evaluate(resolved.sessionId, `(() => {
237
247
  const btns = document.querySelectorAll('button, [role="tab"], [role="menuitem"]');
238
248
  for (const b of btns) {
239
249
  if (${labelSearch}) { b.click(); return 'selected'; }
240
250
  }
241
251
  return 'not found — use browser_snapshot to see available options';
242
- })()`);
252
+ })()`, resolved.tabId);
243
253
 
244
254
  if (selectResult.error) return text(`Failed to select reference type: ${selectResult.error}. Use browser_snapshot to see the panel.`);
245
255
  await wait(1000);
246
256
  }
247
257
 
248
258
  // Step 3: Find file input and upload
249
- const uploadResult = await ni.upload(sessionId, 'input[type="file"]', filePaths);
259
+ const uploadResult = await ni.upload(resolved.sessionId, 'input[type="file"]', filePaths, undefined, resolved.tabId);
250
260
  if (uploadResult.error) {
251
261
  // Fallback: try broader selector
252
- const fallback = await ni.upload(sessionId, 'input[accept*="image"]', filePaths);
262
+ const fallback = await ni.upload(resolved.sessionId, 'input[accept*="image"]', filePaths, undefined, resolved.tabId);
253
263
  if (fallback.error) {
254
264
  return text(
255
265
  `Failed to upload reference image(s): ${uploadResult.error}. ` +
@@ -16013,6 +16013,55 @@
16013
16013
  padding: 4px 0;
16014
16014
  }
16015
16015
 
16016
+ /* ── Effort row — think level chips ── */
16017
+ .as-launch-efforts { display: flex; gap: 4px; }
16018
+ .as-launch-effort {
16019
+ flex: 1;
16020
+ display: flex; flex-direction: column; align-items: center; gap: 2px;
16021
+ padding: 6px 4px 5px;
16022
+ background: rgba(255,255,255,0.03);
16023
+ border: 1px solid rgba(255,255,255,0.06);
16024
+ border-radius: 12px;
16025
+ cursor: pointer; transition: all 0.18s ease;
16026
+ backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
16027
+ }
16028
+ .as-launch-effort:hover {
16029
+ background: rgba(255,255,255,0.06);
16030
+ border-color: rgba(255,255,255,0.10);
16031
+ }
16032
+ .as-launch-effort.active {
16033
+ background: rgba(255,255,255,0.08);
16034
+ border-color: rgba(255,255,255,0.14);
16035
+ box-shadow: var(--shadow-xs), var(--shadow-highlight);
16036
+ }
16037
+ .as-launch-effort.active .as-launch-effort-name {
16038
+ color: var(--t-bright);
16039
+ }
16040
+ .as-launch-effort-name {
16041
+ font-size: 10px; font-weight: 600;
16042
+ color: var(--t-secondary);
16043
+ transition: color 0.15s;
16044
+ }
16045
+ .as-launch-effort-desc {
16046
+ font-size: 8px; font-weight: 400;
16047
+ color: var(--t-faint);
16048
+ transition: color 0.15s;
16049
+ }
16050
+ /* Max effort — warm accent like Opus */
16051
+ .as-launch-effort--top.active .as-launch-effort-name {
16052
+ color: hsla(40, 55%, 78%, 0.95);
16053
+ }
16054
+ .as-launch-effort--top.active .as-launch-effort-desc {
16055
+ color: var(--t-secondary);
16056
+ }
16057
+ /* Default effort (medium) — cool accent */
16058
+ .as-launch-effort--default.active .as-launch-effort-name {
16059
+ color: hsla(210, 45%, 80%, 0.95);
16060
+ }
16061
+ .as-launch-effort--default.active .as-launch-effort-desc {
16062
+ color: var(--t-secondary);
16063
+ }
16064
+
16016
16065
  /* ── Browser mode ── */
16017
16066
  .as-launch-browser-mode { margin-top: 2px; }
16018
16067
  .as-launch-browser-modes { display: flex; gap: 4px; }
@@ -178,9 +178,19 @@ const CLI_MODELS = {
178
178
  ],
179
179
  };
180
180
 
181
+ // Effort levels for Claude models (extended thinking)
182
+ const EFFORT_LEVELS = [
183
+ { id: 'off', label: 'Off', desc: 'No thinking' },
184
+ { id: 'low', label: 'Low', desc: 'Minimal' },
185
+ { id: 'medium', label: 'Med', desc: 'Balanced', tier: 'default' },
186
+ { id: 'high', label: 'High', desc: 'Deep' },
187
+ { id: 'max', label: 'Max', desc: 'Maximum', tier: 'top' },
188
+ ];
189
+
181
190
  // Persistent launch preferences (remembered across launches in session)
182
191
  let _launchProfile = storage.getItem('as-launch-profile') || 'claude-code';
183
192
  let _launchModel = storage.getItem('as-launch-model') || null;
193
+ let _launchEffort = storage.getItem('as-launch-effort') || 'off';
184
194
 
185
195
  // ── Preset Templates with Wizard Steps ──
186
196
 
@@ -785,6 +795,12 @@ function showLaunchInline(params) {
785
795
  ${renderModelChips(profile, currentModel)}
786
796
  </div>
787
797
  </div>
798
+ <div class="as-launch-section" id="as-launch-effort-section" style="${profile === 'claude-code' ? '' : 'display:none'}">
799
+ <label class="as-launch-label">Think</label>
800
+ <div class="as-launch-efforts" id="as-launch-efforts">
801
+ ${renderEffortChips(_launchEffort)}
802
+ </div>
803
+ </div>
788
804
  <div class="as-launch-section as-launch-summary">
789
805
  <div class="as-launch-summary-row">
790
806
  <span class="as-launch-summary-key">Task</span>
@@ -856,6 +872,15 @@ function renderModelChips(profileId, selectedModel) {
856
872
  `).join('');
857
873
  }
858
874
 
875
+ function renderEffortChips(selectedEffort) {
876
+ return EFFORT_LEVELS.map(e => `
877
+ <button class="as-launch-effort${e.id === selectedEffort ? ' active' : ''}${e.tier ? ` as-launch-effort--${e.tier}` : ''}" data-effort="${e.id}">
878
+ <span class="as-launch-effort-name">${e.label}</span>
879
+ <span class="as-launch-effort-desc">${e.desc}</span>
880
+ </button>
881
+ `).join('');
882
+ }
883
+
859
884
  function wireLaunchInline(container) {
860
885
  const root = container.querySelector('.as-launch-inline');
861
886
  if (!root) return;
@@ -892,10 +917,14 @@ function wireLaunchInline(container) {
892
917
  modelsContainer.innerHTML = renderModelChips(profileId, _launchModel);
893
918
  wireModelChips(root);
894
919
  }
920
+ // Show/hide effort section based on profile (Claude only)
921
+ const effortSection = root.querySelector('#as-launch-effort-section');
922
+ if (effortSection) effortSection.style.display = profileId === 'claude-code' ? '' : 'none';
895
923
  });
896
924
  });
897
925
 
898
926
  wireModelChips(root);
927
+ wireEffortChips(root);
899
928
 
900
929
  // Agent submode toggle (Single vs Loop)
901
930
  root.querySelectorAll('.as-launch-agent-submode').forEach(btn => {
@@ -926,6 +955,17 @@ function wireModelChips(container) {
926
955
  });
927
956
  }
928
957
 
958
+ function wireEffortChips(container) {
959
+ container.querySelectorAll('.as-launch-effort').forEach(btn => {
960
+ btn.addEventListener('click', () => {
961
+ container.querySelectorAll('.as-launch-effort').forEach(b => b.classList.remove('active'));
962
+ btn.classList.add('active');
963
+ _launchEffort = btn.dataset.effort;
964
+ storage.setItem('as-launch-effort', _launchEffort);
965
+ });
966
+ });
967
+ }
968
+
929
969
  function closeLaunchInline() {
930
970
  _pendingLaunchParams = null;
931
971
  _launchPanelActive = false;
@@ -940,7 +980,8 @@ function closeLaunchInline() {
940
980
 
941
981
  async function confirmLaunch() {
942
982
  if (!_pendingLaunchParams) return;
943
- const params = { ..._pendingLaunchParams, profile: _launchProfile, model: _launchModel };
983
+ const effort = (_launchProfile === 'claude-code' && _launchEffort && _launchEffort !== 'off') ? _launchEffort : undefined;
984
+ const params = { ..._pendingLaunchParams, profile: _launchProfile, model: _launchModel, effort };
944
985
 
945
986
  // Read agent values BEFORE closing (closeLaunchInline re-renders detail view)
946
987
  const withSynabun = !!document.getElementById('as-agent-synabun')?.checked;
@@ -960,6 +1001,7 @@ async function confirmLaunch() {
960
1001
  const result = await launchAgent({
961
1002
  task: params.task,
962
1003
  model: params.model || undefined,
1004
+ effort: params.effort || undefined,
963
1005
  cwd: params.cwd || undefined,
964
1006
  maxTurns: params.usesBrowser ? 100 : 75,
965
1007
  withSynabun,
@@ -2886,6 +2928,9 @@ async function handlePanelClick(e) {
2886
2928
  const defaultModel = models.find(m => m.tier === 'default') || models[0];
2887
2929
  _launchModel = defaultModel?.id || null;
2888
2930
  storage.setItem('as-launch-model', _launchModel);
2931
+ // Show/hide effort section (Claude only)
2932
+ const qtEffortSec = $('as-qt-effort-section');
2933
+ if (qtEffortSec) qtEffortSec.style.display = profileId === 'claude-code' ? '' : 'none';
2889
2934
  renderSchedulesMain();
2890
2935
  break;
2891
2936
  }
@@ -2901,6 +2946,16 @@ async function handlePanelClick(e) {
2901
2946
  break;
2902
2947
  }
2903
2948
 
2949
+ case 'qt-effort': {
2950
+ const effortId = btn.dataset.effort;
2951
+ if (!effortId) break;
2952
+ _launchEffort = effortId;
2953
+ storage.setItem('as-launch-effort', _launchEffort);
2954
+ $('as-qt-efforts')?.querySelectorAll('.as-launch-effort').forEach(b => b.classList.remove('active'));
2955
+ btn.classList.add('active');
2956
+ break;
2957
+ }
2958
+
2904
2959
  case 'qt-select': {
2905
2960
  const mins = Number(btn.dataset.minutes);
2906
2961
  if (!mins) break;
@@ -2924,8 +2979,9 @@ async function handlePanelClick(e) {
2924
2979
  const qtProfile = _launchProfile || 'claude-code';
2925
2980
  const qtModel = _launchModel || null;
2926
2981
  const qtBrowser = !!$('as-qt-browser')?.checked;
2982
+ const qtEffort = (qtProfile === 'claude-code' && _launchEffort && _launchEffort !== 'off') ? _launchEffort : undefined;
2927
2983
  try {
2928
- const result = await createQuickTimer(templateId, mins, { profile: qtProfile, model: qtModel, usesBrowser: qtBrowser });
2984
+ const result = await createQuickTimer(templateId, mins, { profile: qtProfile, model: qtModel, usesBrowser: qtBrowser, effort: qtEffort });
2929
2985
  // Guard: WebSocket sync:quick-timer:set may arrive before this HTTP response
2930
2986
  if (!_quickTimers.some(t => t.id === result.timerId)) {
2931
2987
  _quickTimers.push({ id: result.timerId, templateId, templateName: result.templateName, firesAt: result.firesAt, minutes: result.minutes, profile: result.profile, model: result.model, usesBrowser: result.usesBrowser });
@@ -2945,8 +3001,9 @@ async function handlePanelClick(e) {
2945
3001
  const qtProfile = _launchProfile || 'claude-code';
2946
3002
  const qtModel = _launchModel || null;
2947
3003
  const qtBrowser = !!$('as-qt-browser')?.checked;
3004
+ const qtEffort = (qtProfile === 'claude-code' && _launchEffort && _launchEffort !== 'off') ? _launchEffort : undefined;
2948
3005
  try {
2949
- const result = await triggerQuickTimerNow(templateId, { profile: qtProfile, model: qtModel, usesBrowser: qtBrowser });
3006
+ const result = await triggerQuickTimerNow(templateId, { profile: qtProfile, model: qtModel, usesBrowser: qtBrowser, effort: qtEffort });
2950
3007
  showToast(`Firing now: ${result.templateName}`);
2951
3008
  } catch (err) { showToast('Run Now failed: ' + err.message); }
2952
3009
  break;
@@ -3260,6 +3317,13 @@ function renderSchedulesMain() {
3260
3317
  </button>
3261
3318
  `).join('');
3262
3319
 
3320
+ const qtEffortChips = EFFORT_LEVELS.map(e => `
3321
+ <button class="as-launch-effort${e.id === _launchEffort ? ' active' : ''}${e.tier ? ` as-launch-effort--${e.tier}` : ''}" data-action="qt-effort" data-effort="${e.id}">
3322
+ <span class="as-launch-effort-name">${e.label}</span>
3323
+ <span class="as-launch-effort-desc">${e.desc}</span>
3324
+ </button>
3325
+ `).join('');
3326
+
3263
3327
  // Determine browser toggle default from selected template
3264
3328
  const selectedTpl = $('as-qt-template')?.value;
3265
3329
  const selectedTemplate = selectedTpl ? allTemplates.find(t => t.id === selectedTpl) : null;
@@ -3279,6 +3343,10 @@ function renderSchedulesMain() {
3279
3343
  <span class="as-launch-label">Model</span>
3280
3344
  <div class="as-launch-models" id="as-qt-models">${qtModelChips}</div>
3281
3345
  </div>
3346
+ <div class="as-qt-group" id="as-qt-effort-section" style="${qtProfile === 'claude-code' ? '' : 'display:none'}">
3347
+ <span class="as-launch-label">Think</span>
3348
+ <div class="as-launch-efforts" id="as-qt-efforts">${qtEffortChips}</div>
3349
+ </div>
3282
3350
  <div class="as-qt-group">
3283
3351
  <span class="as-launch-label">Browser</span>
3284
3352
  <label class="as-qt-browser-toggle">
@@ -3546,33 +3546,25 @@ function renderPostPlanActions(tab, headerText) {
3546
3546
  }
3547
3547
  } else if (a.action === 'plan') {
3548
3548
  // Open plan file in SynaBun code editor for direct editing
3549
- // If tab doesn't have planFilePath (plan was text-only, no Write to ~/.claude/plans/),
3550
- // fall back to scanning plans dir for the most recently modified file
3551
3549
  const openPlan = (path) => emit('open-plan-editor', { filePath: path, tabId: tab.id });
3552
3550
  const noFile = () => { appendStatus(tab, 'No plan file found — send your edits in chat instead.'); card.style.opacity = '1'; card.style.pointerEvents = 'auto'; };
3553
3551
  if (tab.planFilePath) {
3554
3552
  openPlan(tab.planFilePath);
3555
3553
  } else {
3556
- // Fallback chain: scan disk → materialize from DOM → error
3557
- fetch('/api/latest-plan').then(r => r.json()).then(data => {
3558
- if (data.found) {
3559
- tab.planFilePath = data.path;
3560
- openPlan(data.path);
3561
- } else {
3562
- // No file on disk — try to extract plan text from conversation and create one
3563
- const planText = extractPlanText(tab);
3564
- if (!planText) { noFile(); return; }
3565
- fetch('/api/create-plan', {
3566
- method: 'POST',
3567
- headers: { 'Content-Type': 'application/json' },
3568
- body: JSON.stringify({ content: planText }),
3569
- }).then(r => r.json()).then(result => {
3570
- if (result.ok) {
3571
- tab.planFilePath = result.path;
3572
- openPlan(result.path);
3573
- } else { noFile(); }
3574
- }).catch(noFile);
3575
- }
3554
+ // Use eagerly captured plan content (captured at ExitPlanMode time before post-plan
3555
+ // messages pollute the DOM), or extract from DOM as last resort
3556
+ const planText = tab._planContent || extractPlanText(tab);
3557
+ if (!planText) { noFile(); return; }
3558
+ fetch('/api/create-plan', {
3559
+ method: 'POST',
3560
+ headers: { 'Content-Type': 'application/json' },
3561
+ body: JSON.stringify({ content: planText }),
3562
+ }).then(r => r.json()).then(result => {
3563
+ if (result.ok) {
3564
+ tab.planFilePath = result.path;
3565
+ saveTabs();
3566
+ openPlan(result.path);
3567
+ } else { noFile(); }
3576
3568
  }).catch(noFile);
3577
3569
  return; // async — don't fall through
3578
3570
  }
@@ -3732,6 +3724,16 @@ function renderAssistant(tab, msg) {
3732
3724
  pruneMessages($msgs);
3733
3725
 
3734
3726
  if (msgId) { tab.currentMsgId = msgId; tab.currentMsgEl = el; }
3727
+
3728
+ // Eagerly capture plan content the moment ExitPlanMode is detected.
3729
+ // At this point the plan text is the last substantial .msg-body in the DOM —
3730
+ // post-plan messages (remember, recall, stop hook) haven't been added yet.
3731
+ if (tab._exitPlanPending && !tab._planContentCaptured) {
3732
+ tab._planContentCaptured = true;
3733
+ const captured = extractPlanText(tab);
3734
+ if (captured) tab._planContent = captured;
3735
+ }
3736
+
3735
3737
  if (tab === activeTab()) scrollEnd();
3736
3738
  }
3737
3739
 
@@ -4887,7 +4889,7 @@ function _updateSendIcon($send, $input, forceShift) {
4887
4889
 
4888
4890
  function finishTab(tab, skipNotif) {
4889
4891
  const wasRunning = tab.running;
4890
- hideThinking(tab); setRunning(tab, false); tab._wasRunning = false; tab.currentMsgEl = null; tab.currentMsgId = null; tab.pendingAskToolUseId = null; tab.pendingAskRequestId = null; tab.pendingAskBufferedAnswer = null; tab.askRenderedViaControl = false; tab.sendStartedAt = null; tab._exitPlanMsgId = null; tab._exitPlanPending = false; tab._exitPlanHandled = false; tab._exitPlanWasPlanMode = false; if (tab._stream?.mdTimer) clearTimeout(tab._stream.mdTimer); tab._stream = null; saveTabs();
4892
+ hideThinking(tab); setRunning(tab, false); tab._wasRunning = false; tab.currentMsgEl = null; tab.currentMsgId = null; tab.pendingAskToolUseId = null; tab.pendingAskRequestId = null; tab.pendingAskBufferedAnswer = null; tab.askRenderedViaControl = false; tab.sendStartedAt = null; tab._exitPlanMsgId = null; tab._exitPlanPending = false; tab._exitPlanHandled = false; tab._exitPlanWasPlanMode = false; tab._planContentCaptured = false; if (tab._stream?.mdTimer) clearTimeout(tab._stream.mdTimer); tab._stream = null; saveTabs();
4891
4893
  if (wasRunning && !skipNotif) notify('panel', NOTIF_TYPE.DONE, tab.label || 'Claude Code', { tabId: tab.id });
4892
4894
  }
4893
4895
 
@@ -4947,6 +4949,7 @@ function send({ shift = false } = {}) {
4947
4949
  if (text === '/plan') {
4948
4950
  $input.value = ''; hideSlashHints();
4949
4951
  tab.planMode = !tab.planMode;
4952
+ if (tab.planMode) { tab._planContent = null; tab._planContentCaptured = false; tab.planFilePath = ''; tab._editedPlanContent = null; }
4950
4953
  const $plan = _panel?.querySelector('#cp-plan-toggle');
4951
4954
  if ($plan) $plan.classList.toggle('active', tab.planMode);
4952
4955
  appendStatus(tab, tab.planMode ? 'Plan mode ON — Claude will plan without making changes' : 'Plan mode OFF');
@@ -5830,6 +5833,7 @@ function wireEvents() {
5830
5833
  const tab = activeTab();
5831
5834
  if (!tab) return;
5832
5835
  tab.planMode = !tab.planMode;
5836
+ if (tab.planMode) { tab._planContent = null; tab._planContentCaptured = false; tab.planFilePath = ''; tab._editedPlanContent = null; }
5833
5837
  const $plan = _panel?.querySelector('#cp-plan-toggle');
5834
5838
  if ($plan) $plan.classList.toggle('active', tab.planMode);
5835
5839
  appendStatus(tab, tab.planMode ? 'Plan mode ON — Claude will plan without making changes' : 'Plan mode OFF');
@@ -6004,6 +6008,7 @@ function wireEvents() {
6004
6008
  const tab = activeTab();
6005
6009
  if (!tab) return;
6006
6010
  tab.planMode = !tab.planMode;
6011
+ if (tab.planMode) { tab._planContent = null; tab._planContentCaptured = false; tab.planFilePath = ''; tab._editedPlanContent = null; }
6007
6012
  $plan.classList.toggle('active', tab.planMode);
6008
6013
  });
6009
6014
  }
@@ -6244,6 +6249,7 @@ function wireEvents() {
6244
6249
 
6245
6250
  // Store edited content for when user clicks "Continue with implementation"
6246
6251
  tab._editedPlanContent = content;
6252
+ tab._planContent = content;
6247
6253
  if (filePath) tab.planFilePath = filePath;
6248
6254
 
6249
6255
  // Render synthetic assistant message showing the updated plan
@@ -4865,16 +4865,16 @@ export async function openSettingsModal() {
4865
4865
 
4866
4866
  /** Show/hide Chrome-running warning based on selected profile type */
4867
4867
  function updateChromeRunningWarning(chromeRunning, userDataDir) {
4868
- const warn = overlay.querySelector('#bc-chrome-running-warn');
4869
- if (!warn) return;
4870
- // Only warn if Chrome is running AND a real Chrome profile is selected (not sandbox, not SynaBun)
4871
- const isChrome = userDataDir && userDataDir !== '' && !userDataDir.includes('data/chrome-profile') && !userDataDir.includes('data/browser-profiles');
4872
- if (chromeRunning && isChrome) {
4873
- warn.textContent = 'Chrome is running — your profile will be mirrored (synced copy). Cookies and auth stay fresh. Close Chrome to use the real profile directly.';
4874
- warn.style.display = '';
4875
- } else {
4876
- warn.style.display = 'none';
4877
- }
4868
+ const warn = overlay.querySelector('#bc-chrome-running-warn');
4869
+ if (!warn) return;
4870
+ // Only warn if Chrome is running AND a real Chrome profile is selected (not sandbox, not SynaBun)
4871
+ const isChrome = userDataDir && userDataDir !== '' && !userDataDir.includes('data/chrome-profile') && !userDataDir.includes('data/browser-profiles');
4872
+ if (chromeRunning && isChrome) {
4873
+ warn.textContent = 'Chrome is running — SynaBun will open an isolated mirrored copy of the selected profile. Mirror sessions keep their own cookies and auth. Close Chrome to use the real profile directly.';
4874
+ warn.style.display = '';
4875
+ } else {
4876
+ warn.style.display = 'none';
4877
+ }
4878
4878
  }
4879
4879
 
4880
4880
  /** Set the path directly (from typing or browse). Highlights matching profile or deselects all. */
@@ -2215,13 +2215,15 @@ async function openBrowserSession(url, fresh, _unused, force) {
2215
2215
  const _visibilityHandler = null;
2216
2216
 
2217
2217
  // Build tab label with profile info
2218
- let tabLabel = 'Browser';
2219
- if (profileMode === 'mirror' && profileSource) {
2220
- tabLabel = `Browser (${profileSource})`;
2221
- showTermToast(`Synced copy of ${profileSource} — Chrome is running`);
2222
- } else if (profileMode === 'direct' && profileSource) {
2223
- tabLabel = `Browser (${profileSource})`;
2224
- }
2218
+ let tabLabel = 'Browser';
2219
+ if (profileMode === 'mirror' && profileSource) {
2220
+ tabLabel = `Browser (${profileSource})`;
2221
+ showTermToast(`Mirrored copy of ${profileSource}`);
2222
+ } else if (profileMode === 'direct' && profileSource) {
2223
+ tabLabel = `Browser (${profileSource})`;
2224
+ } else if (profileMode === 'managed' && profileSource) {
2225
+ tabLabel = `Browser (${profileSource})`;
2226
+ }
2225
2227
 
2226
2228
  // Register session
2227
2229
  const session = {