Skip to content

Commit 4a48be8

Browse files
feat(launcher): improve table UX, add new CLI flags, and proxy input enhancements (#97)
* refactor: rename BotBrowserConsole to BotBrowserLauncher * feat: add kernel management, proxy management, and setup scripts * fix: enable macOS native Edit menu for Cmd+C/V/X/A keyboard shortcuts * chore: upgrade Neutralino runtime to 6.4.0 * chore: remove Neutralino binaries from repo, download via neu update * fix: improve macOS clipboard support with JS fallback for Cmd+V paste * fix: expand macOS clipboard fallback to Cmd+C/X, auto-download Neutralino via postinstall * chore: upgrade Neutralino to 6.5.0 and simplify setup scripts * fix: fix PowerShell setup script ZIP download corruption * feat: enhance proxy workflow with IP check, quick change, and reusable input component * fix: correct checkbox color override and setup script stdin handling * feat: improve profile deletion with auto-stop, group WebRTC settings, prioritize custom exec path * feat: add port protection toggle, fix kernel auto-update cleanup and UI refresh * fix: detect kernel assets by file extension to support renamed Windows/Linux packages * fix: handle non-browser process exits, add crash detection, validate kernel executable paths * docs: add --bot-time-seed documentation, changelog 2026-03-03, value range and per-context cross-links * feat: add proxy save/check, improve table layout and kernel date display - Add "Save to proxy list" button in proxy input for reusing manually entered proxies - Add batch proxy connectivity check with IP/status display in proxy management - Sort proxy list by creation time (newest first) - Fix kernel date display: parse from asset filename, fix timezone offset bug - Simplify profile status column to icon-only (play/stop) with tooltips - Remove year from "Last Launch" date to prevent truncation - Widen window to 1080px, use fixed table layout to prevent horizontal scrollbar - Add column width constraints for profile and proxy tables * feat: redesign profile editor with left nav, advanced config modes, and new fields - Add left-side anchor navigation with scrollspy (IntersectionObserver) - Split Fingerprint into separate Noise and Rendering sections - Replace expansion panel with flat Advanced section layout - Add mode toggles for Executable (kernel/custom), Cookies and Bookmarks (file/input) - Add FPS dropdown (profile/real/custom number) instead of free-text input - Add Save IP button in proxy section for explicit IP saving - Add new fields: Time Seed, Proxy Bypass Regex, Cookies, Bookmarks, Custom Headers - Put username/password on same row in proxy input - Widen status column to prevent icon clipping * fix: replace Save IP with Save to proxy list, unify dropdown option labels * fix: include username and password in proxy duplicate detection Normalize both sides with `|| ''` to handle undefined values, and add password to the comparison so proxies with the same host:port but different credentials are treated as distinct entries. * fix: prevent editing running profiles and remove auto-save IP on proxy check - Block profile editing when browser is running/launching/stopping, show alert dialog instead of silently darkening the background - Remove onIpCheckResult that auto-saved checked IP to proxyIp field * feat(launcher): add automatic self-update with GitHub commit tracking Check for launcher updates on startup and every hour by comparing the local commit hash against the latest launcher-specific commit from GitHub API (path=launcher). When an update is found, silently download, rebuild, and prompt the user to restart. - Add UpdateService with periodic check, ZIP download, and rebuild - Show update status (checking/downloading/building/ready) in sidebar - Display current version (commit hash) at sidebar bottom - Update setup scripts to save commit hash after install/build - Fix sidebar footer layout to stay at bottom via flex container * feat(launcher): improve table UX, add new CLI flags, and proxy input enhancements - Separate row highlight (single click) from checkbox selection (batch ops) - Add double-click to edit on both profile and proxy tables - Add clear proxy button in profile editor - Combine proxy type/host/port into single row layout - Add --bot-stack-seed and --bot-network-info-override CLI flags - Update noise seed from float to integer range - Stack seed uses dropdown (profile/real/custom) like FPS - Rearrange noise settings: seed+scale+timeseed row, fps+stackseed row - Widen proxy type column in proxy list table
1 parent e63a63c commit 4a48be8

12 files changed

Lines changed: 135 additions & 47 deletions

launcher/angular.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
},
4343
{
4444
"type": "anyComponentStyle",
45-
"maximumWarning": "2kB",
46-
"maximumError": "4kB"
45+
"maximumWarning": "4kB",
46+
"maximumError": "6kB"
4747
}
4848
],
4949
"outputHashing": "none"

launcher/src/app/app.component.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@
126126
<mat-icon>add</mat-icon>
127127
New Profile
128128
</button>
129-
<button mat-button (click)="editSelectedProfile()" [disabled]="selection.selected.length != 1">
129+
<button mat-button (click)="editSelectedProfile()" [disabled]="!highlightedId">
130130
<mat-icon>edit</mat-icon>
131131
Edit
132132
</button>
@@ -315,7 +315,9 @@
315315
<tr
316316
mat-row
317317
*matRowDef="let element; columns: displayedColumns"
318-
(click)="toggleSelectProfile(element)"
318+
[class.row-selected]="highlightedId === element.id"
319+
(click)="selectRow(element)"
320+
(dblclick)="editProfile(element)"
319321
></tr>
320322
</table>
321323
}

launcher/src/app/app.component.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export class AppComponent implements AfterViewInit {
7777
readonly dataSource = new MatTableDataSource<BrowserProfile>([]);
7878
readonly selection = new SelectionModel<BrowserProfile>(true, []);
7979

80+
highlightedId: string | null = null;
8081
loading = false;
8182

8283
@ViewChild(MatSort) set sort(sort: MatSort) {
@@ -116,6 +117,7 @@ export class AppComponent implements AfterViewInit {
116117
}
117118

118119
editProfile(browserProfile: BrowserProfile): void {
120+
this.highlightedId = browserProfile.id;
119121
const status = this.browserLauncherService.getRunningStatus(browserProfile);
120122
if (status !== BrowserProfileStatus.Idle && status !== BrowserProfileStatus.LaunchFailed) {
121123
this.#dialog.open(AlertDialogComponent, {
@@ -132,11 +134,13 @@ export class AppComponent implements AfterViewInit {
132134
.afterClosed()
133135
.subscribe((result) => {
134136
if (!result) return;
137+
this.highlightedId = browserProfile.id;
135138
this.refreshProfiles().catch(console.error);
136139
});
137140
}
138141

139142
changeProxy(browserProfile: BrowserProfile): void {
143+
this.highlightedId = browserProfile.id;
140144
this.#dialog
141145
.open(QuickProxyChangeComponent, {
142146
width: '560px',
@@ -146,20 +150,21 @@ export class AppComponent implements AfterViewInit {
146150
.afterClosed()
147151
.subscribe((result) => {
148152
if (!result) return;
153+
this.highlightedId = browserProfile.id;
149154
this.refreshProfiles().catch(console.error);
150155
});
151156
}
152157

153158
editSelectedProfile(): void {
154-
if (this.selection.selected.length !== 1) {
155-
throw new Error('Please select one profile to edit');
156-
}
157-
158-
this.editProfile(this.selection.selected[0]!);
159+
const profile = this.highlightedId
160+
? this.dataSource.data.find((p) => p.id === this.highlightedId)
161+
: null;
162+
if (!profile) return;
163+
this.editProfile(profile);
159164
}
160165

161-
toggleSelectProfile(browserProfile: BrowserProfile): void {
162-
this.selection.toggle(browserProfile);
166+
selectRow(browserProfile: BrowserProfile): void {
167+
this.highlightedId = browserProfile.id;
163168
}
164169

165170
cloneProfile(browserProfile: BrowserProfile): void {
@@ -285,11 +290,11 @@ export class AppComponent implements AfterViewInit {
285290
this.loading = true;
286291
try {
287292
const profiles = await this.#browserProfileService.getAllBrowserProfiles();
288-
const selectedIds = this.selection.selected.map((profile) => profile.id);
293+
// Preserve checkbox checked state across refresh
294+
const checkedIds = this.selection.selected.map((profile) => profile.id);
289295
this.dataSource.data = profiles;
290-
291296
this.selection.clear();
292-
this.selection.select(...profiles.filter((profile) => selectedIds.includes(profile.id)));
297+
this.selection.select(...profiles.filter((profile) => checkedIds.includes(profile.id)));
293298
} finally {
294299
this.loading = false;
295300
}

launcher/src/app/data/browser-profile.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export interface BehaviorToggles {
7676
botInjectRandomHistory?: boolean;
7777
botDisableConsoleMessage?: boolean;
7878
botPortProtection?: boolean;
79+
botNetworkInfoOverride?: boolean;
7980
}
8081

8182
// Identity & Locale config
@@ -121,6 +122,7 @@ export interface NoiseConfig {
121122
botTimeScale?: number;
122123
botFps?: string;
123124
botTimeSeed?: number;
125+
botStackSeed?: string;
124126
}
125127

126128
// Rendering & Media config

launcher/src/app/edit-browser-profile.component.html

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ <h2 mat-dialog-title>{{ isEdit ? 'Edit' : 'Create' }} Browser Profile</h2>
8989
[showQuickParse]="true"
9090
[showCheckButton]="true"
9191
[showSaveButton]="true"
92+
[showClearButton]="true"
9293
(valueChange)="onProxyValueChange($event)"
9394
(saveToList)="onSaveProxyToList($event)"
95+
(clearProxy)="onClearProxy()"
9496
/>
9597

9698
<div class="section-title">Advanced Proxy Config</div>
@@ -327,21 +329,21 @@ <h2 mat-dialog-title>{{ isEdit ? 'Edit' : 'Create' }} Browser Profile</h2>
327329
<div class="form-row margin-top">
328330
<mat-form-field class="flex-1">
329331
<mat-label>Noise Seed</mat-label>
330-
<input matInput type="number" step="0.01" placeholder="1.0 - 1.2" formControlName="botNoiseSeed" />
331-
<mat-hint>Float for deterministic RNG</mat-hint>
332+
<input matInput type="number" placeholder="0 = profile defaults" formControlName="botNoiseSeed" />
333+
<mat-hint>Integer seed for deterministic noise</mat-hint>
332334
</mat-form-field>
333335

334336
<mat-form-field class="flex-1">
335337
<mat-label>Time Scale</mat-label>
336-
<input
337-
matInput
338-
type="number"
339-
step="0.01"
340-
placeholder="0.80 - 0.99"
341-
formControlName="botTimeScale"
342-
/>
338+
<input matInput type="number" step="0.01" placeholder="0.80 - 0.99" formControlName="botTimeScale" />
343339
<mat-hint>Scale performance.now()</mat-hint>
344340
</mat-form-field>
341+
342+
<mat-form-field class="flex-1">
343+
<mat-label>Time Seed</mat-label>
344+
<input matInput type="number" placeholder="0 = disabled" formControlName="botTimeSeed" />
345+
<mat-hint>Timing diversity (0 disables)</mat-hint>
346+
</mat-form-field>
345347
</div>
346348

347349
<div class="form-row margin-top">
@@ -363,10 +365,22 @@ <h2 mat-dialog-title>{{ isEdit ? 'Edit' : 'Create' }} Browser Profile</h2>
363365
}
364366

365367
<mat-form-field class="flex-1">
366-
<mat-label>Time Seed</mat-label>
367-
<input matInput type="number" placeholder="0 = disabled" formControlName="botTimeSeed" />
368-
<mat-hint>Integer seed for timing diversity (0 disables)</mat-hint>
368+
<mat-label>Stack Seed</mat-label>
369+
<mat-select [(value)]="stackSeedMode" (selectionChange)="onStackSeedModeChange()">
370+
<mat-option value="">Default</mat-option>
371+
<mat-option value="profile">profile</mat-option>
372+
<mat-option value="real">real</mat-option>
373+
<mat-option value="number">Custom</mat-option>
374+
</mat-select>
375+
<mat-hint>JS stack depth fingerprint</mat-hint>
369376
</mat-form-field>
377+
378+
@if (stackSeedMode === 'number') {
379+
<mat-form-field class="flex-1">
380+
<mat-label>Stack Seed Value</mat-label>
381+
<input matInput type="number" placeholder="e.g. 12345" formControlName="botStackSeed" />
382+
</mat-form-field>
383+
}
370384
</div>
371385
</form>
372386
</div>
@@ -480,6 +494,11 @@ <h2 mat-dialog-title>{{ isEdit ? 'Edit' : 'Create' }} Browser Profile</h2>
480494
Port Protection
481495
<span class="toggle-hint">Protect local ports from scanning (default: off)</span>
482496
</mat-slide-toggle>
497+
498+
<mat-slide-toggle formControlName="botNetworkInfoOverride">
499+
Network Info Override
500+
<span class="toggle-hint">Use profile-defined navigator.connection values (default: off)</span>
501+
</mat-slide-toggle>
483502
</form>
484503
</div>
485504

launcher/src/app/edit-browser-profile.component.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ export class EditBrowserProfileComponent implements OnInit, AfterViewInit, OnDes
154154
botInjectRandomHistory: this.#injectedData?.launchOptions?.behavior?.botInjectRandomHistory,
155155
botDisableConsoleMessage: this.#injectedData?.launchOptions?.behavior?.botDisableConsoleMessage ?? true,
156156
botPortProtection: this.#injectedData?.launchOptions?.behavior?.botPortProtection,
157+
botNetworkInfoOverride: this.#injectedData?.launchOptions?.behavior?.botNetworkInfoOverride,
157158
});
158159

159160
// Identity & Locale - default: browserBrand=chrome
@@ -202,6 +203,7 @@ export class EditBrowserProfileComponent implements OnInit, AfterViewInit, OnDes
202203
botTimeScale: this.#injectedData?.launchOptions?.noise?.botTimeScale,
203204
botFps: this.#injectedData?.launchOptions?.noise?.botFps,
204205
botTimeSeed: this.#injectedData?.launchOptions?.noise?.botTimeSeed,
206+
botStackSeed: this.#injectedData?.launchOptions?.noise?.botStackSeed,
205207
});
206208

207209
// FPS mode derived from botFps value
@@ -212,6 +214,14 @@ export class EditBrowserProfileComponent implements OnInit, AfterViewInit, OnDes
212214
return '' as const;
213215
})();
214216

217+
// Stack Seed mode derived from botStackSeed value
218+
stackSeedMode: '' | 'profile' | 'real' | 'number' = (() => {
219+
const seed = this.#injectedData?.launchOptions?.noise?.botStackSeed;
220+
if (seed === 'profile' || seed === 'real') return seed;
221+
if (seed) return 'number' as const;
222+
return '' as const;
223+
})();
224+
215225
// Rendering & Media - defaults: webgl/webgpu/speechVoices/mediaDevices/webrtc=profile, mediaTypes=expand
216226
readonly renderingMediaGroup = this.#formBuilder.group<RenderingMediaConfig>({
217227
botConfigWebgl: this.#injectedData?.launchOptions?.renderingMedia?.botConfigWebgl ?? 'profile',
@@ -345,6 +355,16 @@ export class EditBrowserProfileComponent implements OnInit, AfterViewInit, OnDes
345355
}
346356
}
347357

358+
onStackSeedModeChange(): void {
359+
if (this.stackSeedMode === 'profile' || this.stackSeedMode === 'real') {
360+
this.noiseGroup.patchValue({ botStackSeed: this.stackSeedMode });
361+
} else if (this.stackSeedMode === 'number') {
362+
this.noiseGroup.patchValue({ botStackSeed: '' });
363+
} else {
364+
this.noiseGroup.patchValue({ botStackSeed: '' });
365+
}
366+
}
367+
348368
onProxySelected(proxyId: string): void {
349369
if (!proxyId) {
350370
return;
@@ -366,6 +386,12 @@ export class EditBrowserProfileComponent implements OnInit, AfterViewInit, OnDes
366386
this.selectedProxyId = '';
367387
}
368388

389+
onClearProxy(): void {
390+
this.proxyValue = null;
391+
this.selectedProxyId = '';
392+
this.proxyConfigGroup.patchValue({ proxyIp: '', botIpService: '' });
393+
}
394+
369395
async onSaveProxyToList(proxy: ParsedProxy): Promise<void> {
370396
const duplicate = this.proxies.find(
371397
(p) => p.host === proxy.host && p.port === proxy.port && (p.username || '') === (proxy.username || '') && (p.password || '') === (proxy.password || '')

launcher/src/app/proxy-management/proxy-management.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
Bulk Import
1010
</button>
1111

12-
<button mat-button (click)="editSelectedProxy()" [disabled]="selection.selected.length != 1">
12+
<button mat-button (click)="editSelectedProxy()" [disabled]="!highlightedId">
1313
<mat-icon>edit</mat-icon>
1414
Edit
1515
</button>
@@ -120,7 +120,7 @@
120120
</ng-container>
121121

122122
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
123-
<tr mat-row *matRowDef="let element; columns: displayedColumns" (click)="toggleSelectProxy(element)"></tr>
123+
<tr mat-row *matRowDef="let element; columns: displayedColumns" [class.row-selected]="highlightedId === element.id" (click)="selectRow(element)" (dblclick)="editProxy(element)"></tr>
124124
</table>
125125
}
126126
</div>

launcher/src/app/proxy-management/proxy-management.component.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ mat-toolbar {
2323
}
2424

2525
.mat-column-type {
26-
width: 72px;
27-
max-width: 72px;
26+
width: 130px;
27+
max-width: 130px;
2828
}
2929

3030
.mat-column-username {

launcher/src/app/proxy-management/proxy-management.component.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export class ProxyManagementComponent implements AfterViewInit {
5151
readonly selection = new SelectionModel<Proxy>(true, []);
5252
readonly checkResults = new Map<string, { status: 'checking' | 'ok' | 'fail'; result?: ProxyCheckResult; error?: string }>();
5353

54+
highlightedId: string | null = null;
5455
loading = false;
5556
checking = false;
5657

@@ -89,10 +90,11 @@ export class ProxyManagementComponent implements AfterViewInit {
8990
this.loading = true;
9091
try {
9192
const proxies = await this.#proxyService.getAllProxies();
92-
const selectedIds = this.selection.selected.map((p) => p.id);
93+
// Preserve checkbox checked state across refresh
94+
const checkedIds = this.selection.selected.map((p) => p.id);
9395
this.dataSource.data = proxies;
9496
this.selection.clear();
95-
this.selection.select(...proxies.filter((p) => selectedIds.includes(p.id)));
97+
this.selection.select(...proxies.filter((p) => checkedIds.includes(p.id)));
9698
} finally {
9799
this.loading = false;
98100
}
@@ -117,17 +119,24 @@ export class ProxyManagementComponent implements AfterViewInit {
117119
}
118120

119121
editProxy(proxy: Proxy): void {
122+
this.highlightedId = proxy.id;
120123
this.#dialog
121124
.open(EditProxyComponent, { data: proxy })
122125
.afterClosed()
123-
.subscribe(() => {
126+
.subscribe((result) => {
127+
if (result) {
128+
this.highlightedId = proxy.id;
129+
}
124130
this.refreshProxies().catch(console.error);
125131
});
126132
}
127133

128134
editSelectedProxy(): void {
129-
if (this.selection.selected.length !== 1) return;
130-
this.editProxy(this.selection.selected[0]!);
135+
const proxy = this.highlightedId
136+
? this.dataSource.data.find((p) => p.id === this.highlightedId)
137+
: null;
138+
if (!proxy) return;
139+
this.editProxy(proxy);
131140
}
132141

133142
deleteProxy(proxy: Proxy): void {
@@ -194,8 +203,8 @@ export class ProxyManagementComponent implements AfterViewInit {
194203
this.checking = false;
195204
}
196205

197-
toggleSelectProxy(proxy: Proxy): void {
198-
this.selection.toggle(proxy);
206+
selectRow(proxy: Proxy): void {
207+
this.highlightedId = proxy.id;
199208
}
200209

201210
get isAllSelected(): boolean {

launcher/src/app/shared/browser-launcher.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ export class BrowserLauncherService {
278278
if (opts?.behavior?.botInjectRandomHistory) args.push('--bot-inject-random-history');
279279
if (opts?.behavior?.botDisableConsoleMessage) args.push('--bot-disable-console-message');
280280
if (opts?.behavior?.botPortProtection) args.push('--bot-port-protection');
281+
if (opts?.behavior?.botNetworkInfoOverride) args.push('--bot-network-info-override');
281282

282283
// Identity & Locale
283284
if (opts?.identityLocale?.botConfigBrowserBrand)
@@ -335,6 +336,7 @@ export class BrowserLauncherService {
335336
if (opts?.noise?.botFps) args.push(`--bot-fps=${opts.noise.botFps}`);
336337
if (opts?.noise?.botTimeSeed != null && opts.noise.botTimeSeed !== 0)
337338
args.push(`--bot-time-seed=${opts.noise.botTimeSeed}`);
339+
if (opts?.noise?.botStackSeed) args.push(`--bot-stack-seed=${opts.noise.botStackSeed}`);
338340

339341
// Rendering & Media
340342
if (opts?.renderingMedia?.botConfigWebgl)

0 commit comments

Comments
 (0)