feat: enable remote SDK and resilient device orchestration
This commit is contained in:
1 parent
3cc711df51
commit
ddfa420945
110 files changed
+14450
-12954
No files matched your search
@@ -674,15 +674,24 @@
|
||||
<div class="step-title-row">
|
||||
<h3 class="step-title">Android Device & Emulator Connection</h3>
|
||||
</div>
|
||||
<p class="step-desc">Physical Android phone or local Android Virtual Device (AVD)</p>
|
||||
<p class="step-desc">Local emulator, attached phone, wireless device, or another ADB server</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-restart-adb btn-connection-options"
|
||||
(click)="toggleConnectionMethods()"
|
||||
>
|
||||
<span class="material-symbols-outlined">swap_horiz</span>
|
||||
<span>{{ showConnectionMethods() ? 'Hide Options' : 'Change Connection' }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="btn-restart-adb"
|
||||
[disabled]="isRestartingAdb()"
|
||||
(click)="restartAdbServer()"
|
||||
title="Restart local ADB daemon"
|
||||
[title]="isRemoteAdbServer() ? 'Refresh devices without stopping the remote ADB server' : 'Restart local ADB daemon'"
|
||||
>
|
||||
@if (isRestartingAdb()) {
|
||||
<span class="material-symbols-outlined spinning">sync</span>
|
||||
@@ -692,7 +701,7 @@
|
||||
<span class="feedback-text">{{ adbRestartFeedback() }}</span>
|
||||
} @else {
|
||||
<span class="material-symbols-outlined">sync</span>
|
||||
<span>Restart ADB</span>
|
||||
<span>{{ isRemoteAdbServer() ? 'Refresh Devices' : 'Restart ADB' }}</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@@ -726,6 +735,12 @@
|
||||
<span class="meta-dot">·</span>
|
||||
<span>Android {{ activeDevice()?.android_version }}</span>
|
||||
}
|
||||
@if (isRemoteAdbServer()) {
|
||||
<span class="meta-dot">·</span>
|
||||
<span class="remote-endpoint-label">
|
||||
Remote via {{ adbServerStatus()?.endpoint?.host }}:{{ adbServerStatus()?.endpoint?.port }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-hero-status">
|
||||
@@ -919,7 +934,7 @@
|
||||
|
||||
<div class="failed-actions-bar">
|
||||
@if (emulatorLaunchState()?.avd_name) {
|
||||
<button type="button" class="btn-retry-launch" (click)="launchAvdEmulator(emulatorLaunchState()!.avd_name!)">
|
||||
<button type="button" class="btn-retry-launch" [disabled]="isRemoteAdbServer()" (click)="launchAvdEmulator(emulatorLaunchState()!.avd_name!)">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
<span>Retry Launch ({{ emulatorLaunchState()?.avd_name }})</span>
|
||||
</button>
|
||||
@@ -937,7 +952,7 @@
|
||||
}
|
||||
|
||||
<!-- STATE B-3: NO DEVICE DETECTED / READY TO CONNECT GUIDE -->
|
||||
@if (!isDeviceReady() && !isEmulatorLaunching() && adbProbe()?.summary !== 'Device Booting' && adbProbe()?.summary !== 'Device Unauthorized' && adbProbe()?.summary !== 'Device Locked' && adbProbe()?.summary !== 'Lock State Unknown' && adbProbe()?.status !== 'fail' && emulatorLaunchState()?.status !== 'failed') {
|
||||
@if (showConnectionMethods() || (!isDeviceReady() && !isEmulatorLaunching() && adbProbe()?.summary !== 'Device Booting' && adbProbe()?.summary !== 'Device Unauthorized' && adbProbe()?.summary !== 'Device Locked' && adbProbe()?.summary !== 'Lock State Unknown' && adbProbe()?.status !== 'fail' && emulatorLaunchState()?.status !== 'failed')) {
|
||||
<div class="foolproof-guide-container">
|
||||
<!-- Method Selection Nav Cards -->
|
||||
<div class="connection-methods-nav">
|
||||
@@ -954,13 +969,14 @@
|
||||
<div class="method-info">
|
||||
<div class="method-title-row">
|
||||
<span class="method-title">Android Emulator</span>
|
||||
@if (installedAvds().length > 0) {
|
||||
<span class="method-badge-pill detected">{{ installedAvds().length }} DETECTED</span>
|
||||
} @else {
|
||||
<span class="method-badge-pill recommended">RECOMMENDED</span>
|
||||
}
|
||||
</div>
|
||||
<span class="method-desc">Launch local Android Virtual Device</span>
|
||||
<span class="method-desc">
|
||||
@if (installedAvds().length > 0) {
|
||||
{{ installedAvds().length }} local virtual device{{ installedAvds().length > 1 ? 's' : '' }} available
|
||||
} @else {
|
||||
Launch a local Android Virtual Device
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="method-card-right">
|
||||
@@ -981,7 +997,6 @@
|
||||
<div class="method-info">
|
||||
<div class="method-title-row">
|
||||
<span class="method-title">USB Cable</span>
|
||||
<span class="method-badge-pill neutral">4 STEPS</span>
|
||||
</div>
|
||||
<span class="method-desc">Connect physical phone via USB</span>
|
||||
</div>
|
||||
@@ -1004,7 +1019,6 @@
|
||||
<div class="method-info">
|
||||
<div class="method-title-row">
|
||||
<span class="method-title">Wireless ADB</span>
|
||||
<span class="method-badge-pill info">WI-FI</span>
|
||||
</div>
|
||||
<span class="method-desc">Connect over local Wi-Fi</span>
|
||||
</div>
|
||||
@@ -1013,8 +1027,52 @@
|
||||
<span class="radio-indicator" [class.checked]="activeAdbGuideTab() === 'wifi'"></span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="method-nav-card"
|
||||
[class.active]="activeAdbGuideTab() === 'remote'"
|
||||
(click)="setAdbGuideTab('remote')"
|
||||
>
|
||||
<div class="method-card-left">
|
||||
<div class="method-icon-box">
|
||||
<span class="material-symbols-outlined">devices</span>
|
||||
</div>
|
||||
<div class="method-info">
|
||||
<div class="method-title-row">
|
||||
<span class="method-title">Remote Computer</span>
|
||||
</div>
|
||||
<span class="method-desc">Phone connected to another computer</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="method-card-right">
|
||||
<span class="radio-indicator" [class.checked]="activeAdbGuideTab() === 'remote'"></span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (activeAdbGuideTab() !== 'remote' && isRemoteAdbServer()) {
|
||||
<div class="active-remote-notice">
|
||||
<div class="active-remote-copy">
|
||||
<span class="material-symbols-outlined">hub</span>
|
||||
<span>
|
||||
Artemis is currently using
|
||||
<strong>{{ adbServerStatus()?.endpoint?.host }}:{{ adbServerStatus()?.endpoint?.port }}</strong>.
|
||||
Switch to local ADB before using a local emulator, USB cable, or Wireless ADB.
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-use-local-adb"
|
||||
[disabled]="isSwitchingToLocalAdb()"
|
||||
(click)="switchToLocalAdbServer()"
|
||||
>
|
||||
<span class="material-symbols-outlined" [class.spinning]="isSwitchingToLocalAdb()">sync</span>
|
||||
<span>{{ isSwitchingToLocalAdb() ? 'Switching...' : 'Use Local ADB' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Method 1: Emulator -->
|
||||
@if (activeAdbGuideTab() === 'emulator') {
|
||||
<div class="method-details-panel">
|
||||
@@ -1049,7 +1107,7 @@
|
||||
type="button"
|
||||
class="btn-avd-launch"
|
||||
[class.launching]="launchingAvd() === avd || (isEmulatorLaunching() && emulatorLaunchState()?.avd_name === avd)"
|
||||
[disabled]="isEmulatorLaunching()"
|
||||
[disabled]="isEmulatorLaunching() || isRemoteAdbServer()"
|
||||
(click)="launchAvdEmulator(avd)"
|
||||
[title]="isEmulatorLaunching() ? 'An emulator is currently launching...' : 'Launch ' + avd"
|
||||
>
|
||||
@@ -1319,7 +1377,7 @@
|
||||
</div>
|
||||
|
||||
<div class="wifi-btn-col">
|
||||
<button type="button" class="btn-wifi-connect" [disabled]="isConnectingWifi() || !wifiHost().trim()"
|
||||
<button type="button" class="btn-wifi-connect" [disabled]="isConnectingWifi() || isRemoteAdbServer() || !wifiHost().trim()"
|
||||
(click)="connectWifiDevice()">
|
||||
@if (isConnectingWifi()) {
|
||||
<span class="material-symbols-outlined spinning">sync</span>
|
||||
@@ -1354,6 +1412,132 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Method 4: ADB server on another computer -->
|
||||
@if (activeAdbGuideTab() === 'remote') {
|
||||
<div class="method-details-panel remote-adb-panel">
|
||||
<div class="remote-guide-intro">
|
||||
<span class="material-symbols-outlined intro-icon">hub</span>
|
||||
<span>Enter the host and port of an ADB server endpoint that this computer can access, so Artemis can discover and control a phone physically connected to another computer.</span>
|
||||
</div>
|
||||
|
||||
<div class="remote-adb-interactive-box">
|
||||
<div class="remote-adb-form-row">
|
||||
<div class="remote-adb-input-col host-col">
|
||||
<label class="remote-adb-label" for="remote-adb-host">ADB Server Host</label>
|
||||
<div class="remote-adb-input-wrap">
|
||||
<span class="material-symbols-outlined wrap-icon">dns</span>
|
||||
<input
|
||||
id="remote-adb-host"
|
||||
type="text"
|
||||
class="remote-adb-input"
|
||||
[ngModel]="remoteAdbHost()"
|
||||
(ngModelChange)="updateRemoteAdbHost($event)"
|
||||
placeholder="127.0.0.1"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="remote-adb-input-col port-col">
|
||||
<label class="remote-adb-label" for="remote-adb-port">ADB Server Port</label>
|
||||
<div class="remote-adb-input-wrap">
|
||||
<span class="material-symbols-outlined wrap-icon">tag</span>
|
||||
<input
|
||||
id="remote-adb-port"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
class="remote-adb-input"
|
||||
[ngModel]="remoteAdbPort()"
|
||||
(ngModelChange)="updateRemoteAdbPort($event)"
|
||||
placeholder="5038"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="remote-adb-btn-col">
|
||||
<button
|
||||
type="button"
|
||||
class="btn-remote-adb-connect"
|
||||
[disabled]="isConnectingRemoteAdb() || isActivatingRemoteAdb() || !remoteAdbHost().trim()"
|
||||
(click)="connectRemoteAdbServer()"
|
||||
>
|
||||
@if (isConnectingRemoteAdb()) {
|
||||
<span class="material-symbols-outlined spinning">sync</span>
|
||||
<span>Checking...</span>
|
||||
} @else {
|
||||
<span class="material-symbols-outlined">bolt</span>
|
||||
<span>Test Connection</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="remember-remote-adb">
|
||||
<input
|
||||
type="checkbox"
|
||||
[ngModel]="rememberRemoteAdb()"
|
||||
(ngModelChange)="rememberRemoteAdb.set($event)"
|
||||
/>
|
||||
<span>Remember this endpoint for future Artemis launches</span>
|
||||
</label>
|
||||
|
||||
@if (remoteAdbMessage()) {
|
||||
<div class="remote-adb-status-alert success">
|
||||
<span class="material-symbols-outlined">check_circle</span>
|
||||
<span>{{ remoteAdbMessage() }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (remoteAdbError()) {
|
||||
<div class="remote-adb-status-alert error" role="alert">
|
||||
<span class="material-symbols-outlined">error</span>
|
||||
<span>{{ remoteAdbError() }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (remoteAdbDevices().length > 0) {
|
||||
<div class="remote-device-results">
|
||||
@for (device of remoteAdbDevices(); track device.serial) {
|
||||
<div class="remote-device-row">
|
||||
<span class="material-symbols-outlined">phone_android</span>
|
||||
<div class="remote-device-copy">
|
||||
<strong>{{ device.model || device.serial }}</strong>
|
||||
<span>{{ device.serial }}</span>
|
||||
</div>
|
||||
<span class="remote-device-state" [class.ready]="device.state === 'device'">
|
||||
{{ device.state === 'device' ? 'Ready' : device.state }}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (remoteAdbProbeResult()?.success && !isProbedRemoteAdbActive()) {
|
||||
<button
|
||||
type="button"
|
||||
class="btn-remote-adb-connect btn-use-tested-endpoint"
|
||||
[disabled]="isActivatingRemoteAdb()"
|
||||
(click)="activateRemoteAdbServer()"
|
||||
>
|
||||
@if (isActivatingRemoteAdb()) {
|
||||
<span class="material-symbols-outlined spinning">sync</span>
|
||||
<span>Connecting...</span>
|
||||
} @else {
|
||||
<span class="material-symbols-outlined">check</span>
|
||||
<span>{{ remoteAdbHasReadyDevice() ? 'Use Connection' : 'Use Anyway' }}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<p class="remote-adb-explanation">
|
||||
Artemis sends all ADB commands to this server instead of the default local server. Any phone visible to that server becomes available to Artemis.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1416,7 +1600,7 @@
|
||||
</div>
|
||||
<button type="button" class="btn-retry-auth" (click)="restartAdbServer()">
|
||||
<span class="material-symbols-outlined">refresh</span>
|
||||
<span>Restart ADB & Check</span>
|
||||
<span>{{ isRemoteAdbServer() ? 'Refresh Devices' : 'Restart ADB & Check' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -110,12 +110,16 @@ $text-light: #94a3b8;
|
||||
|
||||
@mixin glass-primary-btn {
|
||||
background: linear-gradient(135deg, rgba(26, 115, 232, 0.92) 0%, rgba(37, 99, 235, 0.85) 100%);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
backdrop-filter: blur(10px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(180%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.28);
|
||||
box-shadow: 0 4px 14px rgba(26, 115, 232, 0.32), inset 0 1px 1px rgba(255, 255, 255, 0.45);
|
||||
color: #ffffff !important;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: background 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
span, .material-symbols-outlined, .run-icon, .action-icon {
|
||||
color: #ffffff !important;
|
||||
@@ -146,12 +150,17 @@ $text-light: #94a3b8;
|
||||
|
||||
@mixin glass-secondary-btn {
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
backdrop-filter: blur(14px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(14px) saturate(180%);
|
||||
backdrop-filter: blur(10px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(180%);
|
||||
border: 1px solid rgba(226, 232, 240, 0.88);
|
||||
box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.05), inset 0 1px 1px rgba(255, 255, 255, 0.95);
|
||||
color: $text-secondary;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: background 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
color 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
@@ -213,10 +222,13 @@ $text-light: #94a3b8;
|
||||
opacity: 0.9;
|
||||
transition: opacity 0.8s ease;
|
||||
|
||||
/* The glow orbs are pre-softened radial gradients. A `filter: blur(90px+)`
|
||||
on top of them looked nearly identical but forced the GPU to re-blur
|
||||
half-megapixel layers on every frame of the float animations — the
|
||||
gradients below bake the same softness in for free. */
|
||||
.glow {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(90px);
|
||||
transform: translate3d(0, 0, 0);
|
||||
will-change: transform, opacity;
|
||||
pointer-events: none;
|
||||
@@ -227,7 +239,7 @@ $text-light: #94a3b8;
|
||||
left: 10%;
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
background: radial-gradient(circle, rgba(59, 130, 246, 0.12) 0%, rgba(99, 102, 241, 0.04) 55%, transparent 75%);
|
||||
background: radial-gradient(circle, rgba(59, 130, 246, 0.12) 0%, rgba(99, 102, 241, 0.04) 45%, transparent 82%);
|
||||
animation: floatGlow1 22s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@@ -236,7 +248,7 @@ $text-light: #94a3b8;
|
||||
right: 12%;
|
||||
width: 560px;
|
||||
height: 560px;
|
||||
background: radial-gradient(circle, rgba(147, 51, 234, 0.08) 0%, rgba(219, 39, 119, 0.03) 55%, transparent 75%);
|
||||
background: radial-gradient(circle, rgba(147, 51, 234, 0.08) 0%, rgba(219, 39, 119, 0.03) 45%, transparent 82%);
|
||||
animation: floatGlow2 26s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@@ -245,7 +257,7 @@ $text-light: #94a3b8;
|
||||
left: 30%;
|
||||
width: 460px;
|
||||
height: 460px;
|
||||
background: radial-gradient(circle, rgba(6, 182, 212, 0.08) 0%, rgba(59, 130, 246, 0.03) 55%, transparent 75%);
|
||||
background: radial-gradient(circle, rgba(6, 182, 212, 0.08) 0%, rgba(59, 130, 246, 0.03) 45%, transparent 82%);
|
||||
animation: floatGlow3 20s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@@ -256,9 +268,8 @@ $text-light: #94a3b8;
|
||||
margin: 0 auto;
|
||||
width: 720px;
|
||||
height: 520px;
|
||||
background: radial-gradient(circle, rgba(245, 158, 11, 0.09) 0%, rgba(217, 119, 6, 0.035) 45%, transparent 75%);
|
||||
background: radial-gradient(circle, rgba(245, 158, 11, 0.09) 0%, rgba(217, 119, 6, 0.035) 38%, transparent 82%);
|
||||
opacity: 0;
|
||||
filter: blur(100px);
|
||||
transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
animation: readyPulseGlow 9s ease-in-out infinite alternate;
|
||||
pointer-events: none;
|
||||
@@ -271,9 +282,8 @@ $text-light: #94a3b8;
|
||||
margin: 0 auto;
|
||||
width: 700px;
|
||||
height: 520px;
|
||||
background: radial-gradient(circle, rgba(16, 185, 129, 0.14) 0%, rgba(6, 182, 212, 0.08) 45%, transparent 75%);
|
||||
background: radial-gradient(circle, rgba(16, 185, 129, 0.14) 0%, rgba(6, 182, 212, 0.08) 38%, transparent 82%);
|
||||
opacity: 0;
|
||||
filter: blur(100px);
|
||||
transition: opacity 1s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
animation: readyPulseGlow 7s ease-in-out infinite alternate;
|
||||
pointer-events: none;
|
||||
@@ -286,9 +296,8 @@ $text-light: #94a3b8;
|
||||
margin: 0 auto;
|
||||
width: 680px;
|
||||
height: 520px;
|
||||
background: radial-gradient(circle, rgba(99, 102, 241, 0.18) 0%, rgba(147, 51, 234, 0.08) 45%, transparent 75%);
|
||||
background: radial-gradient(circle, rgba(99, 102, 241, 0.18) 0%, rgba(147, 51, 234, 0.08) 38%, transparent 82%);
|
||||
opacity: 0;
|
||||
filter: blur(95px);
|
||||
transition: opacity 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -383,8 +392,8 @@ $text-light: #94a3b8;
|
||||
max-width: 520px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
backdrop-filter: blur(16px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||
backdrop-filter: blur(12px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(180%);
|
||||
padding: 3px;
|
||||
border-radius: 9999px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.85);
|
||||
@@ -392,7 +401,9 @@ $text-light: #94a3b8;
|
||||
box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.06), 0 1px 2px rgba(0, 0, 0, 0.02), inset 0 1px 1px rgba(255, 255, 255, 0.9);
|
||||
user-select: none;
|
||||
font-family: 'Google Sans', 'Google Sans Text', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: background 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
@@ -1717,6 +1728,12 @@ $text-light: #94a3b8;
|
||||
.meta-dot {
|
||||
color: $text-light;
|
||||
}
|
||||
|
||||
.remote-endpoint-label {
|
||||
color: $primary;
|
||||
font-family: $font-mono;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1739,6 +1756,7 @@ $text-light: #94a3b8;
|
||||
animation: statusDotPulse 2s infinite cubic-bezier(0.4, 0, 0.6, 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.multi-device-switcher {
|
||||
@@ -2365,10 +2383,14 @@ $text-light: #94a3b8;
|
||||
|
||||
.connection-methods-nav {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
|
||||
@media (max-width: 860px) {
|
||||
@media (max-width: 1100px) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -2421,37 +2443,12 @@ $text-light: #94a3b8;
|
||||
.method-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.method-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: $text-main;
|
||||
}
|
||||
|
||||
.method-badge-pill {
|
||||
font-size: 9.5px;
|
||||
font-weight: 700;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
|
||||
&.detected, &.info {
|
||||
background: $primary-light;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
&.recommended {
|
||||
background: $emerald-light;
|
||||
color: $emerald;
|
||||
}
|
||||
|
||||
&.neutral {
|
||||
background: #f1f5f9;
|
||||
color: $text-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.method-desc {
|
||||
@@ -2511,6 +2508,68 @@ $text-light: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.active-remote-notice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
background: #fffbeb;
|
||||
border: 1px solid $amber-border;
|
||||
border-radius: 8px;
|
||||
|
||||
.active-remote-copy {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
color: #78350f;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.45;
|
||||
|
||||
.material-symbols-outlined {
|
||||
color: $amber;
|
||||
font-size: 17px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-family: $font-mono;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-use-local-adb {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
min-height: 32px;
|
||||
padding: 0 12px;
|
||||
@include glass-secondary-btn;
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
.btn-use-local-adb {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.method-details-panel {
|
||||
background: #ffffff;
|
||||
border: 1px solid $card-border;
|
||||
@@ -2852,7 +2911,8 @@ $text-light: #94a3b8;
|
||||
}
|
||||
|
||||
.usb-guide-intro,
|
||||
.wifi-guide-intro {
|
||||
.wifi-guide-intro,
|
||||
.remote-guide-intro {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
@@ -3086,6 +3146,258 @@ $text-light: #94a3b8;
|
||||
margin-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&.remote-adb-panel {
|
||||
.remote-adb-interactive-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid $card-border;
|
||||
border-radius: 8px;
|
||||
|
||||
.remote-adb-form-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.remote-adb-input-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
|
||||
&.host-col {
|
||||
flex: 1 1 220px;
|
||||
}
|
||||
|
||||
&.port-col {
|
||||
flex: 0 1 120px;
|
||||
}
|
||||
|
||||
.remote-adb-label {
|
||||
color: $text-muted;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.remote-adb-input-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding: 0 9px;
|
||||
background: #ffffff;
|
||||
border: 1px solid $card-border;
|
||||
border-radius: 6px;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:focus-within {
|
||||
border-color: $primary;
|
||||
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.1);
|
||||
}
|
||||
|
||||
.wrap-icon {
|
||||
margin-right: 6px;
|
||||
color: $text-light;
|
||||
font-size: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.remote-adb-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: $text-main;
|
||||
font-family: $font-mono;
|
||||
font-size: 12px;
|
||||
|
||||
&::placeholder {
|
||||
color: $text-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remote-adb-btn-col {
|
||||
flex-shrink: 0;
|
||||
|
||||
.btn-remote-adb-connect {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
@include glass-primary-btn;
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
.remote-adb-input-col,
|
||||
.remote-adb-input-col.host-col,
|
||||
.remote-adb-input-col.port-col,
|
||||
.remote-adb-btn-col {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.remote-adb-btn-col .btn-remote-adb-connect {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remember-remote-adb {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
gap: 7px;
|
||||
color: $text-secondary;
|
||||
font-size: 11.5px;
|
||||
cursor: pointer;
|
||||
|
||||
input {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin: 0;
|
||||
accent-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-use-tested-endpoint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-self: flex-end;
|
||||
gap: 5px;
|
||||
min-width: 150px;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
@include glass-primary-btn;
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.remote-adb-status-alert {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
padding: 7px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 15px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.success {
|
||||
color: $emerald-dark;
|
||||
background: $emerald-light;
|
||||
border: 1px solid $emerald-border;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: $rose-dark;
|
||||
background: $rose-light;
|
||||
border: 1px solid $rose-border;
|
||||
}
|
||||
}
|
||||
|
||||
.remote-device-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
border: 1px solid $card-border;
|
||||
border-radius: 7px;
|
||||
|
||||
.remote-device-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
min-width: 0;
|
||||
padding: 8px 10px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
> .material-symbols-outlined {
|
||||
color: $primary;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.remote-device-copy {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
|
||||
strong {
|
||||
color: $text-main;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
color: $text-muted;
|
||||
font-family: $font-mono;
|
||||
font-size: 10.5px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.remote-device-state {
|
||||
color: $amber-dark;
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
|
||||
&.ready {
|
||||
color: $emerald-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.remote-adb-explanation {
|
||||
margin: 0;
|
||||
color: $text-muted;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3496,13 +3808,15 @@ $text-light: #94a3b8;
|
||||
/* Unified Sculptured Composer (Linear / Claude / ChatGPT Grade) */
|
||||
.composer-card {
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
backdrop-filter: blur(28px) saturate(190%);
|
||||
-webkit-backdrop-filter: blur(28px) saturate(190%);
|
||||
backdrop-filter: blur(16px) saturate(190%);
|
||||
-webkit-backdrop-filter: blur(16px) saturate(190%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.95);
|
||||
box-shadow: 0 20px 50px -12px rgba(15, 23, 42, 0.08), 0 1px 3px rgba(0, 0, 0, 0.02), inset 0 1px 1px rgba(255, 255, 255, 0.95);
|
||||
border-radius: 20px;
|
||||
padding: 18px 20px 14px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: border-color 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
|
||||
&.is-pro-active {
|
||||
@@ -4125,12 +4439,15 @@ $text-light: #94a3b8;
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
backdrop-filter: blur(20px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(180%);
|
||||
backdrop-filter: blur(10px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(180%);
|
||||
border: 1px solid rgba(226, 232, 240, 0.85);
|
||||
box-shadow: 0 2px 8px -1px rgba(15, 23, 42, 0.04), inset 0 1px 1px rgba(255, 255, 255, 0.95);
|
||||
cursor: pointer;
|
||||
transition: all 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition: background 0.22s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
transform 0.22s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.22s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
border-color 0.22s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
user-select: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -4367,18 +4684,17 @@ $text-light: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
/* Transform-only: animating opacity here overrode the .glow-pending/.glow-ready
|
||||
fade transitions and kept both state glows permanently visible. */
|
||||
@keyframes readyPulseGlow {
|
||||
0% {
|
||||
transform: scale(0.97);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.06);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4411,3 +4727,13 @@ $text-light: #94a3b8;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
/* Respect the user's motion preference: freeze the purely decorative ambient
|
||||
animations (aurora drift, pulses, sparkles). Functional spinners keep going. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.home-container .aurora-background .glow,
|
||||
.sparkle-icon,
|
||||
.status-dot {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
@@ -14,13 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, signal, computed, effect, inject, OnInit, OnDestroy } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, signal, computed, effect, inject, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { AgentService } from '../../services/agent.service';
|
||||
import { SystemService } from '../../services/system.service';
|
||||
import { DeviceInfo, ProbeResult } from '../../core/models/system.model';
|
||||
import {
|
||||
AdbServerConnectionResult,
|
||||
AdbServerDevice,
|
||||
DeviceInfo,
|
||||
ProbeResult
|
||||
} from '../../core/models/system.model';
|
||||
import {
|
||||
AppReference,
|
||||
SmartSuggestion,
|
||||
@@ -30,12 +35,15 @@ import { TaskRecommendationService } from '../../core/services/task-recommendati
|
||||
|
||||
export type { AppReference, SmartSuggestion, SuggestionCategory };
|
||||
|
||||
type AdbGuideTab = 'emulator' | 'usb' | 'wifi' | 'remote';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule],
|
||||
imports: [FormsModule],
|
||||
templateUrl: './home.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './home.component.scss'
|
||||
})
|
||||
export class HomeComponent implements OnInit, OnDestroy {
|
||||
@@ -47,8 +55,8 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
// High-level navigation mode: 'diagnostics' (System Setup Guide) vs 'launcher' (Task Execution)
|
||||
public activeTab = signal<'diagnostics' | 'launcher'>('launcher');
|
||||
|
||||
// Interactive guide sub-tab inside ADB section: 'emulator' | 'usb' | 'wifi'
|
||||
public activeAdbGuideTab = signal<'emulator' | 'usb' | 'wifi'>('emulator');
|
||||
// Interactive guide sub-tab inside the ADB section
|
||||
public activeAdbGuideTab = signal<AdbGuideTab>('emulator');
|
||||
public emulatorSetupMode = signal<'studio' | 'cli'>('studio');
|
||||
|
||||
// Interactive guide tab for LLM / OCR credentials: 'gemini' | 'ocr'
|
||||
@@ -90,6 +98,29 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
public wifiConnectMessage = signal<string | null>(null);
|
||||
public wifiConnectError = signal<string | null>(null);
|
||||
public adbRestartFeedback = signal<string | null>(null);
|
||||
public showConnectionMethods = signal<boolean>(false);
|
||||
|
||||
// ADB server endpoint connection state
|
||||
public remoteAdbHost = signal<string>('127.0.0.1');
|
||||
public remoteAdbPort = signal<string>('5038');
|
||||
public rememberRemoteAdb = signal<boolean>(true);
|
||||
public isConnectingRemoteAdb = signal<boolean>(false);
|
||||
public isActivatingRemoteAdb = signal<boolean>(false);
|
||||
public isSwitchingToLocalAdb = signal<boolean>(false);
|
||||
public remoteAdbMessage = signal<string | null>(null);
|
||||
public remoteAdbError = signal<string | null>(null);
|
||||
public remoteAdbDevices = signal<AdbServerDevice[]>([]);
|
||||
public remoteAdbProbeResult = signal<AdbServerConnectionResult | null>(null);
|
||||
public adbServerStatus = computed(() => this.systemService.adbServerStatus());
|
||||
public isRemoteAdbServer = computed(() => this.systemService.isRemoteAdbServer());
|
||||
public remoteAdbHasReadyDevice = computed(() =>
|
||||
this.remoteAdbDevices().some(device => device.state === 'device')
|
||||
);
|
||||
public isProbedRemoteAdbActive = computed(() => {
|
||||
const tested = this.remoteAdbProbeResult()?.endpoint;
|
||||
const active = this.adbServerStatus()?.endpoint;
|
||||
return !!tested && !!active && tested.identity === active.identity;
|
||||
});
|
||||
|
||||
public wifiCommand = computed(() => {
|
||||
const h = this.wifiHost().trim() || '<phone-ip>';
|
||||
@@ -334,6 +365,16 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
// Initial fetch of system readiness & model configuration
|
||||
this.systemService.fetchReadiness().subscribe();
|
||||
this.systemService.fetchModelConfigEnv().subscribe();
|
||||
this.systemService.fetchAdbServerStatus().subscribe({
|
||||
next: status => {
|
||||
if (status.endpoint.mode === 'remote') {
|
||||
this.remoteAdbHost.set(status.endpoint.host);
|
||||
this.remoteAdbPort.set(String(status.endpoint.port));
|
||||
this.activeAdbGuideTab.set('remote');
|
||||
}
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
|
||||
window.addEventListener('focus', this.focusListener);
|
||||
}
|
||||
@@ -354,8 +395,10 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
this.selectedOs.set(os);
|
||||
}
|
||||
|
||||
public setAdbGuideTab(tab: 'emulator' | 'usb' | 'wifi'): void {
|
||||
public setAdbGuideTab(tab: AdbGuideTab): void {
|
||||
this.activeAdbGuideTab.set(tab);
|
||||
this.remoteAdbError.set(null);
|
||||
this.remoteAdbMessage.set(null);
|
||||
}
|
||||
|
||||
public setEmulatorSetupMode(mode: 'studio' | 'cli'): void {
|
||||
@@ -609,7 +652,9 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
this.adbRestartFeedback.set(null);
|
||||
this.systemService.restartAdb().subscribe({
|
||||
next: (res) => {
|
||||
this.adbRestartFeedback.set('ADB Refreshed ✓');
|
||||
this.adbRestartFeedback.set(
|
||||
res?.restart_result?.skipped ? 'Devices Refreshed ✓' : 'ADB Refreshed ✓'
|
||||
);
|
||||
setTimeout(() => this.adbRestartFeedback.set(null), 2500);
|
||||
},
|
||||
error: () => {
|
||||
@@ -620,6 +665,10 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
public connectWifiDevice(): void {
|
||||
if (this.isRemoteAdbServer()) {
|
||||
this.wifiConnectError.set('Switch to local ADB before connecting a Wireless ADB device.');
|
||||
return;
|
||||
}
|
||||
const host = this.wifiHost().trim();
|
||||
const portStr = this.wifiPort().trim() || '5555';
|
||||
const port = parseInt(portStr, 10) || 5555;
|
||||
@@ -651,7 +700,123 @@ export class HomeComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
public connectRemoteAdbServer(): void {
|
||||
const host = this.remoteAdbHost().trim();
|
||||
const port = Number(this.remoteAdbPort().trim());
|
||||
|
||||
if (!host) {
|
||||
this.remoteAdbError.set('Enter the host name or IP address of the ADB server.');
|
||||
return;
|
||||
}
|
||||
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
||||
this.remoteAdbError.set('Enter a port between 1 and 65535.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isConnectingRemoteAdb.set(true);
|
||||
this.remoteAdbError.set(null);
|
||||
this.remoteAdbMessage.set(null);
|
||||
this.remoteAdbDevices.set([]);
|
||||
this.remoteAdbProbeResult.set(null);
|
||||
|
||||
this.systemService.probeAdbServer(host, port).subscribe({
|
||||
next: response => {
|
||||
this.isConnectingRemoteAdb.set(false);
|
||||
const result = response.connection_result;
|
||||
if (result.success) {
|
||||
this.remoteAdbProbeResult.set(result);
|
||||
this.remoteAdbDevices.set(result.devices || []);
|
||||
this.remoteAdbMessage.set(result.message);
|
||||
} else {
|
||||
this.remoteAdbError.set(result.message);
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
this.isConnectingRemoteAdb.set(false);
|
||||
this.remoteAdbError.set(
|
||||
error?.error?.detail || 'Unable to test the ADB server endpoint.'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public activateRemoteAdbServer(): void {
|
||||
const tested = this.remoteAdbProbeResult();
|
||||
if (!tested?.success) {
|
||||
this.remoteAdbError.set('Test the endpoint before using it.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isActivatingRemoteAdb.set(true);
|
||||
this.remoteAdbError.set(null);
|
||||
this.systemService.connectAdbServer(
|
||||
tested.endpoint.host,
|
||||
tested.endpoint.port,
|
||||
this.rememberRemoteAdb()
|
||||
).subscribe({
|
||||
next: response => {
|
||||
this.isActivatingRemoteAdb.set(false);
|
||||
const result = response.connection_result;
|
||||
if (result.success) {
|
||||
this.remoteAdbProbeResult.set(result);
|
||||
this.remoteAdbDevices.set(result.devices || []);
|
||||
this.remoteAdbMessage.set(result.message);
|
||||
} else {
|
||||
this.remoteAdbError.set(result.message);
|
||||
}
|
||||
},
|
||||
error: error => {
|
||||
this.isActivatingRemoteAdb.set(false);
|
||||
this.remoteAdbError.set(
|
||||
error?.error?.detail || 'Unable to use the ADB server endpoint.'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public updateRemoteAdbHost(value: string): void {
|
||||
this.remoteAdbHost.set(value);
|
||||
this.clearRemoteAdbProbe();
|
||||
}
|
||||
|
||||
public updateRemoteAdbPort(value: string): void {
|
||||
this.remoteAdbPort.set(value);
|
||||
this.clearRemoteAdbProbe();
|
||||
}
|
||||
|
||||
private clearRemoteAdbProbe(): void {
|
||||
this.remoteAdbProbeResult.set(null);
|
||||
this.remoteAdbDevices.set([]);
|
||||
this.remoteAdbMessage.set(null);
|
||||
this.remoteAdbError.set(null);
|
||||
}
|
||||
|
||||
public switchToLocalAdbServer(): void {
|
||||
this.isSwitchingToLocalAdb.set(true);
|
||||
this.remoteAdbError.set(null);
|
||||
this.systemService.useLocalAdbServer(true).subscribe({
|
||||
next: response => {
|
||||
this.isSwitchingToLocalAdb.set(false);
|
||||
this.remoteAdbDevices.set([]);
|
||||
this.remoteAdbMessage.set(response.connection_result.message);
|
||||
},
|
||||
error: error => {
|
||||
this.isSwitchingToLocalAdb.set(false);
|
||||
this.remoteAdbError.set(
|
||||
error?.error?.detail || 'Unable to switch back to the local ADB server.'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public toggleConnectionMethods(): void {
|
||||
this.showConnectionMethods.update(value => !value);
|
||||
}
|
||||
|
||||
public launchAvdEmulator(avdName: string): void {
|
||||
if (this.isRemoteAdbServer()) {
|
||||
return;
|
||||
}
|
||||
this.systemService.launchEmulator(avdName).subscribe();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,16 +14,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, HostListener } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, HostListener, ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
import { LegacyAgentStreamComponent } from '../../components/legacy-agent-stream/legacy-agent-stream.component';
|
||||
import { ChatInterfaceComponent } from '../../components/chat-interface/chat-interface.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-legacy-workspace',
|
||||
standalone: true,
|
||||
imports: [CommonModule, LegacyAgentStreamComponent, ChatInterfaceComponent],
|
||||
imports: [LegacyAgentStreamComponent, ChatInterfaceComponent],
|
||||
templateUrl: './legacy-workspace.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.Eager,
|
||||
styleUrl: './legacy-workspace.component.scss'
|
||||
})
|
||||
export class LegacyWorkspaceComponent {
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<!--
|
||||
Copyright 2026 Google LLC
|
||||
Copyright 2026 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<div class="workspace-container" [class.is-resizing]="isDragging">
|
||||
<div class="workspace-container" [class.is-resizing]="isDragging()">
|
||||
<!-- Left Main Panel: Raw Agent Log Stream -->
|
||||
<main class="left-panel glass-panel">
|
||||
<app-agent-stream></app-agent-stream>
|
||||
@@ -23,13 +23,15 @@
|
||||
<div class="workspace-floating-bar-wrapper" [class.is-expanded]="isBarExpanded()" [class.is-dormant]="!isBarExpanded()"
|
||||
[class.is-focused]="isInputFocused()" [class.has-text]="!!taskInput.trim()" [class.is-running]="isTaskRunning()">
|
||||
<!-- Floating Error / Notification Toast -->
|
||||
<div class="floating-dock-toast error" *ngIf="errorMessage">
|
||||
<span class="material-symbols-outlined toast-icon">error</span>
|
||||
<span class="toast-text">{{ errorMessage }}</span>
|
||||
<button type="button" class="btn-toast-close" (click)="errorMessage = null" aria-label="Close error message">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
@if (errorMessage()) {
|
||||
<div class="floating-dock-toast error">
|
||||
<span class="material-symbols-outlined toast-icon">error</span>
|
||||
<span class="toast-text">{{ errorMessage() }}</span>
|
||||
<button type="button" class="btn-toast-close" (click)="errorMessage.set(null)" aria-label="Close error message">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Dock Row (Contains the Main Card and the Separate Stop Button when running) -->
|
||||
<div class="floating-dock-row">
|
||||
@@ -52,9 +54,9 @@
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M 0,25 Q 300,55 600,20 T 1200,25 Q 1500,55 1800,20 T 2400,25 L 2400,80 L 0,80 Z"
|
||||
fill="url(#waveGradBack)"></path>
|
||||
fill="url(#waveGradBack)"></path>
|
||||
</svg>
|
||||
|
||||
|
||||
<!-- Mid Fluid Wave -->
|
||||
<svg class="liquid-wave wave-mid" viewBox="0 0 2400 80" preserveAspectRatio="none">
|
||||
<defs>
|
||||
@@ -66,9 +68,9 @@
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M 0,35 Q 300,5 600,45 T 1200,35 Q 1500,5 1800,45 T 2400,35 L 2400,80 L 0,80 Z"
|
||||
fill="url(#waveGradMid)"></path>
|
||||
fill="url(#waveGradMid)"></path>
|
||||
</svg>
|
||||
|
||||
|
||||
<!-- Front Fluid Wave -->
|
||||
<svg class="liquid-wave wave-front" viewBox="0 0 2400 80" preserveAspectRatio="none">
|
||||
<defs>
|
||||
@@ -80,16 +82,16 @@
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M 0,18 Q 300,50 600,15 T 1200,18 Q 1500,50 1800,15 T 2400,18 L 2400,80 L 0,80 Z"
|
||||
fill="url(#waveGradFront)"></path>
|
||||
fill="url(#waveGradFront)"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Dormant / Capsule Peek View (Visible when mouse is away and input idle) -->
|
||||
<div class="capsule-peek-view">
|
||||
<span class="capsule-placeholder">Start a new task...</span>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Expanded Full View Content (Visible on hover, focus, or text) -->
|
||||
<div class="expanded-card-content">
|
||||
<div class="card-main-input-row">
|
||||
@@ -101,7 +103,7 @@
|
||||
(focus)="onInputFocus()"
|
||||
(blur)="onInputBlur()"
|
||||
rows="1"
|
||||
[disabled]="isSubmitting"
|
||||
[disabled]="isSubmitting()"
|
||||
[placeholder]="selectedProfile() === 'pro'
|
||||
? 'Pro: Multi-agent cognitive graph architecture for complex multi-step tasks'
|
||||
: 'Flash: Reactive fast loop for standard app workflows within 25 steps'"
|
||||
@@ -111,30 +113,38 @@
|
||||
</div>
|
||||
|
||||
<!-- Clear Button (visible when text is entered) -->
|
||||
<button *ngIf="taskInput.trim() && !isSubmitting" type="button" class="dock-circle-btn btn-clear"
|
||||
(click)="clearInput($event)" title="Clear input text" aria-label="Clear input text">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
|
||||
@if (taskInput.trim() && !isSubmitting()) {
|
||||
<button type="button" class="dock-circle-btn btn-clear"
|
||||
(click)="clearInput($event)" title="Clear input text" aria-label="Clear input text">
|
||||
<span class="material-symbols-outlined">close</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Stop Button (Visible when currently viewed task is active) -->
|
||||
<button *ngIf="isTaskRunning()" type="button" class="dock-circle-btn btn-stop" [disabled]="isSubmitting"
|
||||
(click)="stopTask($event)"
|
||||
[title]="stopButtonTitle()"
|
||||
[attr.aria-label]="stopButtonTitle()"
|
||||
>
|
||||
<span class="material-symbols-outlined">stop</span>
|
||||
</button>
|
||||
@if (isTaskRunning()) {
|
||||
<button type="button" class="dock-circle-btn btn-stop" [disabled]="isSubmitting()"
|
||||
(click)="stopTask($event)"
|
||||
[title]="stopButtonTitle()"
|
||||
[attr.aria-label]="stopButtonTitle()"
|
||||
>
|
||||
<span class="material-symbols-outlined">stop</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<!-- Send Button -->
|
||||
<button type="button" class="dock-circle-btn btn-send"
|
||||
[class.has-content]="!!taskInput.trim()"
|
||||
[disabled]="!taskInput.trim() || isSubmitting"
|
||||
[disabled]="!taskInput.trim() || isSubmitting()"
|
||||
(click)="submitTask()"
|
||||
[title]="isSubmitting ? 'Starting task...' : 'Start task (Enter)'"
|
||||
[title]="isSubmitting() ? 'Starting task...' : 'Start task (Enter)'"
|
||||
aria-label="Start task"
|
||||
>
|
||||
<span class="material-symbols-outlined spinning-icon" *ngIf="isSubmitting">sync</span>
|
||||
<span class="material-symbols-outlined" *ngIf="!isSubmitting">arrow_upward</span>
|
||||
>
|
||||
@if (isSubmitting()) {
|
||||
<span class="material-symbols-outlined spinning-icon">sync</span>
|
||||
}
|
||||
@if (!isSubmitting()) {
|
||||
<span class="material-symbols-outlined">arrow_upward</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -162,28 +172,30 @@
|
||||
</div>
|
||||
|
||||
<!-- Separate Standalone Stop Button Beside the Capsule (Visible when currently viewed task is active) -->
|
||||
<button *ngIf="isTaskRunning()" type="button" class="standalone-stop-btn"
|
||||
[disabled]="isSubmitting"
|
||||
(click)="stopTask($event)"
|
||||
[title]="stopButtonTitle()"
|
||||
[attr.aria-label]="stopButtonTitle()"
|
||||
>
|
||||
<span class="material-symbols-outlined">stop</span>
|
||||
</button>
|
||||
@if (isTaskRunning()) {
|
||||
<button type="button" class="standalone-stop-btn"
|
||||
[disabled]="isSubmitting()"
|
||||
(click)="stopTask($event)"
|
||||
[title]="stopButtonTitle()"
|
||||
[attr.aria-label]="stopButtonTitle()"
|
||||
>
|
||||
<span class="material-symbols-outlined">stop</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Split Pane Drag Resizer -->
|
||||
<div class="resizer" (mousedown)="onDragStart($event)">
|
||||
<div class="resizer-handle"></div>
|
||||
</div>
|
||||
|
||||
<!-- Right Sidebar Panel: Chat Interface Mock -->
|
||||
<aside class="right-panel" [style.width.px]="rightPanelWidth">
|
||||
<app-chat-interface></app-chat-interface>
|
||||
</aside>
|
||||
|
||||
<!-- Floating Video Replay Player Modal / Window -->
|
||||
<app-floating-video-player></app-floating-video-player>
|
||||
|
||||
<!-- Split Pane Drag Resizer -->
|
||||
<div class="resizer" (mousedown)="onDragStart($event)">
|
||||
<div class="resizer-handle"></div>
|
||||
</div>
|
||||
|
||||
<!-- Right Sidebar Panel: Chat Interface Mock -->
|
||||
<aside class="right-panel" [style.width.px]="rightPanelWidth()">
|
||||
<app-chat-interface></app-chat-interface>
|
||||
</aside>
|
||||
|
||||
<!-- Floating Video Replay Player Modal / Window -->
|
||||
<app-floating-video-player></app-floating-video-player>
|
||||
</div>
|
||||
@@ -104,7 +104,7 @@ $rose-dark: #dc2626;
|
||||
height: 40px;
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
transition: background 0.2s ease, height 0.2s ease;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +308,11 @@ $rose-dark: #dc2626;
|
||||
background: radial-gradient(ellipse at center, rgba(168, 85, 247, 0.45) 0%, rgba(236, 72, 153, 0.25) 50%, transparent 80%);
|
||||
}
|
||||
|
||||
/* Wave Tracks Wrapper with Soft Gaussian Blur */
|
||||
/* Wave Tracks Wrapper. The soft-liquid look comes from blurring each
|
||||
wave individually: blurring the WRAPPER forced the GPU to re-run the
|
||||
gaussian pass every animation frame (child transforms invalidate the
|
||||
parent's filtered layer), while per-wave blur over transform-only
|
||||
animations is rasterized once and then just composited. */
|
||||
.wave-tracks-wrapper {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
@@ -316,7 +320,6 @@ $rose-dark: #dc2626;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
overflow: hidden;
|
||||
filter: blur(7px); /* Soften sharp vector wave edges into silky liquid light */
|
||||
mask-image: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.5) 60%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0.5) 60%, transparent 100%);
|
||||
|
||||
@@ -327,7 +330,7 @@ $rose-dark: #dc2626;
|
||||
width: 200%;
|
||||
height: 100%;
|
||||
will-change: transform;
|
||||
filter: blur(3px); /* Secondary soft layer blur */
|
||||
filter: blur(8px); /* Silky liquid softness, applied per cached wave layer */
|
||||
|
||||
&.wave-back {
|
||||
opacity: 0.45;
|
||||
@@ -562,7 +565,11 @@ $rose-dark: #dc2626;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transition: background 0.18s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
color 0.18s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
transform 0.18s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
box-shadow 0.18s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
border-color 0.18s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
|
||||
.material-symbols-outlined {
|
||||
font-size: 18px;
|
||||
@@ -695,7 +702,9 @@ $rose-dark: #dc2626;
|
||||
font-size: 11px;
|
||||
font-weight: 550;
|
||||
cursor: pointer;
|
||||
transition: all 0.16s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transition: color 0.16s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
background 0.16s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
box-shadow 0.16s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
outline: none;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
@@ -863,3 +872,14 @@ $rose-dark: #dc2626;
|
||||
transform: scaleX(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
/* Respect the user's motion preference: freeze the decorative liquid waves
|
||||
and pulses while keeping the frosted dock visuals intact. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.liquid-wave,
|
||||
.wave-glow-ambient,
|
||||
.pulse-indicator-dot,
|
||||
.running-pulse-dot {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, HostListener, inject, computed, signal, ViewChild, ElementRef, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy, NgZone, DestroyRef, inject, computed, signal, ViewChild, ElementRef, OnInit } from '@angular/core';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { AgentStreamComponent } from '../../components/agent-stream/agent-stream.component';
|
||||
import { ChatInterfaceComponent } from '../../components/chat-interface/chat-interface.component';
|
||||
@@ -26,26 +26,35 @@ import { AgentService } from '../../services/agent.service';
|
||||
selector: 'app-workspace',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
AgentStreamComponent,
|
||||
ChatInterfaceComponent,
|
||||
FloatingVideoPlayerComponent
|
||||
],
|
||||
],
|
||||
templateUrl: './workspace.component.html',
|
||||
styleUrl: './workspace.component.scss'
|
||||
styleUrl: './workspace.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class WorkspaceComponent implements OnInit {
|
||||
public agentService = inject(AgentService);
|
||||
private zone = inject(NgZone);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
// Default right panel width to 1/3 of the screen (or 450px as fallback)
|
||||
public rightPanelWidth = typeof window !== 'undefined' ? Math.round(window.innerWidth / 3) : 450;
|
||||
public isDragging = false;
|
||||
public rightPanelWidth = signal<number>(
|
||||
typeof window !== 'undefined' ? Math.round(window.innerWidth / 3) : 450
|
||||
);
|
||||
public isDragging = signal<boolean>(false);
|
||||
private dragWidthRafId: number | null = null;
|
||||
private pendingDragWidth = 0;
|
||||
|
||||
// Floating Command Bar State
|
||||
public taskInput: string = '';
|
||||
public isSubmitting: boolean = false;
|
||||
public errorMessage: string | null = null;
|
||||
// Floating Command Bar State. taskInput is backed by a signal so computed
|
||||
// expressions (isBarExpanded) genuinely track it under OnPush.
|
||||
private taskInputSignal = signal<string>('');
|
||||
public get taskInput(): string { return this.taskInputSignal(); }
|
||||
public set taskInput(value: string) { this.taskInputSignal.set(value); }
|
||||
public isSubmitting = signal<boolean>(false);
|
||||
public errorMessage = signal<string | null>(null);
|
||||
public selectedProfile = signal<'flash' | 'pro'>('flash');
|
||||
|
||||
// Expand States (Signals for 0-latency reactivity)
|
||||
@@ -61,6 +70,16 @@ export class WorkspaceComponent implements OnInit {
|
||||
this.selectedProfile.set(saved);
|
||||
}
|
||||
}
|
||||
|
||||
// The global ⌘K/Ctrl+K shortcut is registered outside the Angular zone so
|
||||
// ordinary typing never schedules an extra change-detection pass.
|
||||
this.zone.runOutsideAngular(() => {
|
||||
window.addEventListener('keydown', this.onGlobalKeyDown);
|
||||
});
|
||||
this.destroyRef.onDestroy(() => {
|
||||
window.removeEventListener('keydown', this.onGlobalKeyDown);
|
||||
this.detachDragListeners();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,13 +198,12 @@ export class WorkspaceComponent implements OnInit {
|
||||
/**
|
||||
* Global keyboard shortcut (⌘K or Ctrl+K) to focus input bar from anywhere
|
||||
*/
|
||||
@HostListener('window:keydown', ['$event'])
|
||||
public onGlobalKeyDown(event: KeyboardEvent): void {
|
||||
private onGlobalKeyDown = (event: KeyboardEvent): void => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
|
||||
event.preventDefault();
|
||||
this.focusInput();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle enter key in floating dock textarea
|
||||
@@ -202,12 +220,12 @@ export class WorkspaceComponent implements OnInit {
|
||||
*/
|
||||
public submitTask(): void {
|
||||
const goal = this.taskInput.trim();
|
||||
if (!goal || this.isSubmitting) {
|
||||
if (!goal || this.isSubmitting()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSubmitting = true;
|
||||
this.errorMessage = null;
|
||||
this.isSubmitting.set(true);
|
||||
this.errorMessage.set(null);
|
||||
|
||||
if (this.dockInputRef?.nativeElement) {
|
||||
this.dockInputRef.nativeElement.blur();
|
||||
@@ -220,15 +238,15 @@ export class WorkspaceComponent implements OnInit {
|
||||
if (this.dockInputRef?.nativeElement) {
|
||||
this.dockInputRef.nativeElement.style.height = 'auto';
|
||||
}
|
||||
this.isSubmitting = false;
|
||||
this.isSubmitting.set(false);
|
||||
this.agentService.fetchStatus();
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Failed to submit task:', err);
|
||||
this.isSubmitting = false;
|
||||
this.errorMessage = err.error?.detail || 'The runner is busy. Please wait for current task to finish.';
|
||||
this.isSubmitting.set(false);
|
||||
this.errorMessage.set(err.error?.detail || 'The runner is busy. Please wait for current task to finish.');
|
||||
setTimeout(() => {
|
||||
this.errorMessage = null;
|
||||
this.errorMessage.set(null);
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
@@ -245,28 +263,31 @@ export class WorkspaceComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
const targetSessionId = this.agentService.currentSessionId();
|
||||
this.isSubmitting = true;
|
||||
this.errorMessage = null;
|
||||
this.isSubmitting.set(true);
|
||||
this.errorMessage.set(null);
|
||||
this.agentService.stopTask(targetSessionId, false);
|
||||
setTimeout(() => {
|
||||
this.isSubmitting = false;
|
||||
this.isSubmitting.set(false);
|
||||
}, 400);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle mouse down on resizer bar to start dragging
|
||||
* Handle mouse down on resizer bar to start dragging. The move/up listeners
|
||||
* are attached only for the duration of the drag and run outside the Angular
|
||||
* zone: idle mouse movement over the workspace never triggers change
|
||||
* detection, and drag updates are coalesced to one per animation frame.
|
||||
*/
|
||||
public onDragStart(event: MouseEvent): void {
|
||||
this.isDragging = true;
|
||||
this.isDragging.set(true);
|
||||
event.preventDefault();
|
||||
this.zone.runOutsideAngular(() => {
|
||||
document.addEventListener('mousemove', this.onMouseMove);
|
||||
document.addEventListener('mouseup', this.onMouseUp);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to global mousemove events to adjust the width dynamically
|
||||
*/
|
||||
@HostListener('document:mousemove', ['$event'])
|
||||
public onMouseMove(event: MouseEvent): void {
|
||||
if (!this.isDragging) {
|
||||
private onMouseMove = (event: MouseEvent): void => {
|
||||
if (!this.isDragging()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -276,15 +297,27 @@ export class WorkspaceComponent implements OnInit {
|
||||
|
||||
// Apply boundary limits to prevent panels from shrinking too much
|
||||
if (newWidth >= minWidth && newWidth <= maxWidth) {
|
||||
this.rightPanelWidth = newWidth;
|
||||
this.pendingDragWidth = newWidth;
|
||||
if (this.dragWidthRafId === null) {
|
||||
this.dragWidthRafId = requestAnimationFrame(() => {
|
||||
this.dragWidthRafId = null;
|
||||
this.rightPanelWidth.set(this.pendingDragWidth);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private onMouseUp = (): void => {
|
||||
this.isDragging.set(false);
|
||||
this.detachDragListeners();
|
||||
};
|
||||
|
||||
private detachDragListeners(): void {
|
||||
document.removeEventListener('mousemove', this.onMouseMove);
|
||||
document.removeEventListener('mouseup', this.onMouseUp);
|
||||
if (this.dragWidthRafId !== null) {
|
||||
cancelAnimationFrame(this.dragWidthRafId);
|
||||
this.dragWidthRafId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to global mouseup events to stop resizing
|
||||
*/
|
||||
@HostListener('document:mouseup')
|
||||
public onMouseUp(): void {
|
||||
this.isDragging = false;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user