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.
- package/CHANGELOG.md +51 -52
- package/hooks/claude-code/prompt-submit.mjs +6 -6
- package/mcp-server/dist/services/neural-interface.d.ts +20 -19
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +52 -45
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/tools/browser-advanced.d.ts +4 -0
- package/mcp-server/dist/tools/browser-advanced.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-advanced.js +7 -4
- package/mcp-server/dist/tools/browser-advanced.js.map +1 -1
- package/mcp-server/dist/tools/browser-interact.d.ts +16 -0
- package/mcp-server/dist/tools/browser-interact.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-interact.js +25 -16
- package/mcp-server/dist/tools/browser-interact.js.map +1 -1
- package/mcp-server/dist/tools/browser-navigate.d.ts +8 -0
- package/mcp-server/dist/tools/browser-navigate.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-navigate.js +13 -8
- package/mcp-server/dist/tools/browser-navigate.js.map +1 -1
- package/mcp-server/dist/tools/browser-observe.d.ts +48 -0
- package/mcp-server/dist/tools/browser-observe.d.ts.map +1 -1
- package/mcp-server/dist/tools/browser-observe.js +74 -49
- package/mcp-server/dist/tools/browser-observe.js.map +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts +20 -0
- package/mcp-server/dist/tools/leonardo-browser-tools.d.ts.map +1 -1
- package/mcp-server/dist/tools/leonardo-browser-tools.js +39 -34
- package/mcp-server/dist/tools/leonardo-browser-tools.js.map +1 -1
- package/mcp-server/src/services/neural-interface.ts +58 -46
- package/mcp-server/src/tools/browser-advanced.ts +10 -5
- package/mcp-server/src/tools/browser-interact.ts +34 -24
- package/mcp-server/src/tools/browser-navigate.ts +83 -77
- package/mcp-server/src/tools/browser-observe.ts +99 -73
- package/mcp-server/src/tools/leonardo-browser-tools.ts +47 -37
- package/neural-interface/public/shared/styles.css +49 -0
- package/neural-interface/public/shared/ui-automation-studio.js +71 -3
- package/neural-interface/public/shared/ui-claude-panel.js +29 -23
- package/neural-interface/public/shared/ui-settings.js +10 -10
- package/neural-interface/public/shared/ui-terminal.js +9 -7
- package/neural-interface/server.js +732 -386
- 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
|
|
13
|
+
const tabIdField = z.string().optional().describe('Target a specific tab within the session. Auto-resolved from environment if omitted.');
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
const LEO_BASE = 'https://app.leonardo.ai';
|
|
16
16
|
|
|
17
|
-
|
|
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
|
|
41
|
-
if (
|
|
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
|
|
79
|
-
if (
|
|
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
|
|
119
|
-
if (
|
|
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
|
|
147
|
-
if (
|
|
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
|
|
188
|
-
if (
|
|
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
|
|
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
|
-
//
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
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 —
|
|
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(`
|
|
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 = {
|