@phenx-inc/ctlsurf 0.11.0 → 0.12.0
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/out/headless/index.mjs +111 -5
- package/out/headless/index.mjs.map +3 -3
- package/out/main/index.js +311 -7
- package/out/preload/index.js +9 -0
- package/out/renderer/assets/{cssMode-Bmzy9hKX.js → cssMode-R8fTFLae.js} +3 -3
- package/out/renderer/assets/{freemarker2-CDAsExTa.js → freemarker2-CyjWzWoP.js} +1 -1
- package/out/renderer/assets/{handlebars-CvpBYG1L.js → handlebars-CrhCGIRM.js} +1 -1
- package/out/renderer/assets/{html-CNhEQqr8.js → html-Cc3dPNZr.js} +1 -1
- package/out/renderer/assets/{htmlMode-DD7YPgga.js → htmlMode-BWtG6ywk.js} +3 -3
- package/out/renderer/assets/{index-CGHeDwZa.css → index-BL9njXIL.css} +201 -0
- package/out/renderer/assets/{index-BDlCPzCG.js → index-kkG56aKv.js} +407 -34
- package/out/renderer/assets/{javascript-CsGLsswK.js → javascript-C8T_SdLC.js} +2 -2
- package/out/renderer/assets/{jsonMode-CTuGgRvC.js → jsonMode-D1zR4VVU.js} +3 -3
- package/out/renderer/assets/{liquid-BrrN4uul.js → liquid-COSMe9Uf.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Bhdn2Eqy.js → lspLanguageFeatures-CXGfrtzl.js} +1 -1
- package/out/renderer/assets/{mdx-DJo-gax4.js → mdx-RfS44T-L.js} +1 -1
- package/out/renderer/assets/{python-DYHeg5b2.js → python-DFSWKHUu.js} +1 -1
- package/out/renderer/assets/{razor-Cqcks14K.js → razor-DtH2_uvo.js} +1 -1
- package/out/renderer/assets/{tsMode-YTLgCayh.js → tsMode-BYIQ-S9Z.js} +1 -1
- package/out/renderer/assets/{typescript-oxBn_Vcw.js → typescript-HhQUr1Rb.js} +1 -1
- package/out/renderer/assets/{xml-BEqGMCTc.js → xml-KuhgdKfD.js} +1 -1
- package/out/renderer/assets/{yaml-BGSrk5HC.js → yaml-W_kzCFH7.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/agents.ts +9 -1
- package/src/main/ctlsurfApi.ts +7 -0
- package/src/main/index.ts +58 -3
- package/src/main/orchestrator.ts +105 -4
- package/src/main/ssh.ts +348 -0
- package/src/preload/index.ts +26 -0
- package/src/renderer/App.tsx +20 -0
- package/src/renderer/components/AgentPicker.tsx +18 -0
- package/src/renderer/components/ProjectPicker.tsx +121 -0
- package/src/renderer/components/RemoteConnect.tsx +315 -0
- package/src/renderer/styles.css +201 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
interface AgentConfig {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
command: string
|
|
7
|
+
args: string[]
|
|
8
|
+
description: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SshHost {
|
|
12
|
+
alias: string
|
|
13
|
+
hostName?: string
|
|
14
|
+
user?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ProbeResult {
|
|
18
|
+
reachable: boolean
|
|
19
|
+
authRequired: boolean
|
|
20
|
+
error?: string
|
|
21
|
+
tmuxInstalled: boolean
|
|
22
|
+
sessions: string[]
|
|
23
|
+
ctlsurfSessions: string[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface RemoteConnectProps {
|
|
27
|
+
/** Called with the built ssh-backed agent once an action is chosen. */
|
|
28
|
+
onConnect: (agent: AgentConfig) => void
|
|
29
|
+
/** Return to the normal agent list. */
|
|
30
|
+
onBack: () => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Step = 'hosts' | 'probing' | 'action' | 'error'
|
|
34
|
+
|
|
35
|
+
const TMUX_PREFIX = 'ctlsurf_'
|
|
36
|
+
|
|
37
|
+
// Lowest free ctlsurf_N given the sessions already on the host (mirrors
|
|
38
|
+
// nextSessionName in src/main/ssh.ts so the label is right before we connect).
|
|
39
|
+
function nextSessionName(sessions: string[]): string {
|
|
40
|
+
const used = new Set(
|
|
41
|
+
sessions
|
|
42
|
+
.filter((s) => s.startsWith(TMUX_PREFIX))
|
|
43
|
+
.map((s) => parseInt(s.slice(TMUX_PREFIX.length), 10))
|
|
44
|
+
.filter((n) => Number.isInteger(n) && n > 0),
|
|
45
|
+
)
|
|
46
|
+
let n = 1
|
|
47
|
+
while (used.has(n)) n++
|
|
48
|
+
return `${TMUX_PREFIX}${n}`
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function RemoteConnect({ onConnect, onBack }: RemoteConnectProps) {
|
|
52
|
+
const [step, setStep] = useState<Step>('hosts')
|
|
53
|
+
const [configHosts, setConfigHosts] = useState<SshHost[]>([])
|
|
54
|
+
const [recents, setRecents] = useState<string[]>([])
|
|
55
|
+
const [manual, setManual] = useState('')
|
|
56
|
+
const [agentCmd, setAgentCmd] = useState<'claude' | ''>('claude') // '' = remote shell
|
|
57
|
+
const [target, setTarget] = useState('')
|
|
58
|
+
const [probe, setProbe] = useState<ProbeResult | null>(null)
|
|
59
|
+
const [errorMsg, setErrorMsg] = useState('')
|
|
60
|
+
const [password, setPassword] = useState('')
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
let cancelled = false
|
|
64
|
+
window.worker.sshListHosts().then((r) => {
|
|
65
|
+
if (cancelled) return
|
|
66
|
+
setConfigHosts(r.configHosts || [])
|
|
67
|
+
setRecents(r.recents || [])
|
|
68
|
+
}).catch(() => { /* no hosts */ })
|
|
69
|
+
return () => { cancelled = true }
|
|
70
|
+
}, [])
|
|
71
|
+
|
|
72
|
+
const startProbe = async (t: string) => {
|
|
73
|
+
const dest = t.trim()
|
|
74
|
+
if (!dest) return
|
|
75
|
+
setTarget(dest)
|
|
76
|
+
setStep('probing')
|
|
77
|
+
try {
|
|
78
|
+
const result = await window.worker.sshProbe(dest)
|
|
79
|
+
setProbe(result)
|
|
80
|
+
if (result.reachable || result.authRequired) {
|
|
81
|
+
setStep('action')
|
|
82
|
+
} else {
|
|
83
|
+
setErrorMsg(result.error || 'Could not connect')
|
|
84
|
+
setStep('error')
|
|
85
|
+
}
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
setErrorMsg(err?.message || 'Probe failed')
|
|
88
|
+
setStep('error')
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const connect = async (action: unknown) => {
|
|
93
|
+
try {
|
|
94
|
+
const agent = await window.worker.sshBuildAgent(target, action, agentCmd)
|
|
95
|
+
// Hand the typed password (if any) to the main process for one-shot
|
|
96
|
+
// injection into ssh's prompt, then wipe it from component state. Blank =
|
|
97
|
+
// let ssh prompt in the terminal instead.
|
|
98
|
+
if (password) {
|
|
99
|
+
await window.worker.sshSetPassword(target, password).catch(() => { /* falls back to terminal prompt */ })
|
|
100
|
+
}
|
|
101
|
+
setPassword('')
|
|
102
|
+
window.worker.sshAddRecent(target).catch(() => { /* convenience only */ })
|
|
103
|
+
onConnect(agent)
|
|
104
|
+
} catch (err: any) {
|
|
105
|
+
setErrorMsg(err?.message || 'Failed to build connection')
|
|
106
|
+
setStep('error')
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const passwordField = (
|
|
111
|
+
<div className="remote-pw">
|
|
112
|
+
<input
|
|
113
|
+
className="remote-input"
|
|
114
|
+
type="password"
|
|
115
|
+
placeholder="Password (optional — blank prompts in terminal)"
|
|
116
|
+
value={password}
|
|
117
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
118
|
+
autoCapitalize="off"
|
|
119
|
+
autoCorrect="off"
|
|
120
|
+
spellCheck={false}
|
|
121
|
+
/>
|
|
122
|
+
<div className="remote-pw-hint">Used once for this connection — never stored.</div>
|
|
123
|
+
</div>
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const agentLabel = agentCmd === 'claude' ? 'Claude Code' : 'shell'
|
|
127
|
+
|
|
128
|
+
// ─── Agent toggle (Claude Code vs shell), shared across steps ───
|
|
129
|
+
const agentToggle = (
|
|
130
|
+
<div className="remote-toggle">
|
|
131
|
+
<span className="remote-toggle-label">Run:</span>
|
|
132
|
+
<button
|
|
133
|
+
className={`remote-toggle-btn${agentCmd === 'claude' ? ' is-active' : ''}`}
|
|
134
|
+
onClick={() => setAgentCmd('claude')}
|
|
135
|
+
>Claude Code</button>
|
|
136
|
+
<button
|
|
137
|
+
className={`remote-toggle-btn${agentCmd === '' ? ' is-active' : ''}`}
|
|
138
|
+
onClick={() => setAgentCmd('')}
|
|
139
|
+
>Shell</button>
|
|
140
|
+
</div>
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className="agent-picker-overlay">
|
|
145
|
+
<div className="agent-picker-modal">
|
|
146
|
+
<div className="agent-picker-brand">ctlsurf</div>
|
|
147
|
+
<div className="agent-picker-title">CONNECT TO REMOTE</div>
|
|
148
|
+
|
|
149
|
+
{step === 'hosts' && (
|
|
150
|
+
<>
|
|
151
|
+
{agentToggle}
|
|
152
|
+
|
|
153
|
+
{recents.length > 0 && (
|
|
154
|
+
<>
|
|
155
|
+
<div className="remote-section-label">RECENT</div>
|
|
156
|
+
<div className="agent-picker-list">
|
|
157
|
+
{recents.map((r) => (
|
|
158
|
+
<button key={r} className="agent-picker-item" onClick={() => startProbe(r)}>
|
|
159
|
+
<span className="agent-picker-name">{r}</span>
|
|
160
|
+
<span className="agent-picker-desc">reconnect</span>
|
|
161
|
+
</button>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{configHosts.length > 0 && (
|
|
168
|
+
<>
|
|
169
|
+
<div className="remote-section-label">~/.ssh/config</div>
|
|
170
|
+
<div className="agent-picker-list remote-host-scroll">
|
|
171
|
+
{configHosts.map((h) => (
|
|
172
|
+
<button key={h.alias} className="agent-picker-item" onClick={() => startProbe(h.alias)}>
|
|
173
|
+
<span className="agent-picker-name">{h.alias}</span>
|
|
174
|
+
<span className="agent-picker-desc">
|
|
175
|
+
{h.user ? `${h.user}@` : ''}{h.hostName || ''}
|
|
176
|
+
</span>
|
|
177
|
+
</button>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
</>
|
|
181
|
+
)}
|
|
182
|
+
|
|
183
|
+
<div className="remote-section-label">OR TYPE A HOST</div>
|
|
184
|
+
<form
|
|
185
|
+
className="remote-manual"
|
|
186
|
+
onSubmit={(e) => { e.preventDefault(); startProbe(manual) }}
|
|
187
|
+
>
|
|
188
|
+
<input
|
|
189
|
+
className="remote-input"
|
|
190
|
+
type="text"
|
|
191
|
+
placeholder="user@host"
|
|
192
|
+
value={manual}
|
|
193
|
+
onChange={(e) => setManual(e.target.value)}
|
|
194
|
+
autoCapitalize="off"
|
|
195
|
+
autoCorrect="off"
|
|
196
|
+
spellCheck={false}
|
|
197
|
+
/>
|
|
198
|
+
<button className="remote-btn" type="submit" disabled={!manual.trim()}>Connect</button>
|
|
199
|
+
</form>
|
|
200
|
+
|
|
201
|
+
<button className="remote-link" onClick={onBack}>← back to agents</button>
|
|
202
|
+
</>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
{step === 'probing' && (
|
|
206
|
+
<div className="remote-status">
|
|
207
|
+
<div className="remote-spinner" />
|
|
208
|
+
<div>Connecting to <b>{target}</b>…</div>
|
|
209
|
+
<div className="remote-status-sub">Checking for tmux sessions</div>
|
|
210
|
+
</div>
|
|
211
|
+
)}
|
|
212
|
+
|
|
213
|
+
{step === 'action' && probe && (
|
|
214
|
+
<>
|
|
215
|
+
<div className="remote-target-line">
|
|
216
|
+
<b>{target}</b> · run {agentLabel}
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{probe.authRequired && (
|
|
220
|
+
<>
|
|
221
|
+
<div className="remote-note">
|
|
222
|
+
This host needs a password. Enter it below, or leave blank to
|
|
223
|
+
type it at ssh's prompt in the terminal. Either way it is never stored.
|
|
224
|
+
</div>
|
|
225
|
+
{passwordField}
|
|
226
|
+
</>
|
|
227
|
+
)}
|
|
228
|
+
|
|
229
|
+
{/* Reattach existing sessions (only when we could enumerate them). */}
|
|
230
|
+
{probe.reachable && probe.sessions.length > 0 && (
|
|
231
|
+
<>
|
|
232
|
+
<div className="remote-section-label">REATTACH</div>
|
|
233
|
+
<div className="agent-picker-list">
|
|
234
|
+
{[...probe.sessions]
|
|
235
|
+
.sort((a, b) => (a.startsWith(TMUX_PREFIX) === b.startsWith(TMUX_PREFIX) ? 0 : a.startsWith(TMUX_PREFIX) ? -1 : 1))
|
|
236
|
+
.map((s) => (
|
|
237
|
+
<button
|
|
238
|
+
key={s}
|
|
239
|
+
className="agent-picker-item"
|
|
240
|
+
onClick={() => connect({ kind: 'attach', session: s })}
|
|
241
|
+
>
|
|
242
|
+
<span className="agent-picker-name">{s}</span>
|
|
243
|
+
<span className="agent-picker-desc">attach running tmux session</span>
|
|
244
|
+
</button>
|
|
245
|
+
))}
|
|
246
|
+
</div>
|
|
247
|
+
</>
|
|
248
|
+
)}
|
|
249
|
+
|
|
250
|
+
<div className="remote-section-label">START</div>
|
|
251
|
+
<div className="agent-picker-list">
|
|
252
|
+
{/* New tmux session — when reachable+tmux, or password host (auto). */}
|
|
253
|
+
{((probe.reachable && probe.tmuxInstalled) || probe.authRequired) && (
|
|
254
|
+
<button
|
|
255
|
+
className="agent-picker-item"
|
|
256
|
+
onClick={() => {
|
|
257
|
+
const session = probe.reachable ? nextSessionName(probe.sessions) : `${TMUX_PREFIX}1`
|
|
258
|
+
connect(probe.reachable
|
|
259
|
+
? { kind: 'new-tmux', session }
|
|
260
|
+
: { kind: 'auto', session })
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
<span className="agent-picker-name">
|
|
264
|
+
New tmux session ({probe.reachable ? nextSessionName(probe.sessions) : `${TMUX_PREFIX}1`})
|
|
265
|
+
</span>
|
|
266
|
+
<span className="agent-picker-desc">persists across disconnects (recommended)</span>
|
|
267
|
+
</button>
|
|
268
|
+
)}
|
|
269
|
+
|
|
270
|
+
{probe.reachable && !probe.tmuxInstalled && (
|
|
271
|
+
<div className="remote-note">tmux isn't installed on this host — running without it.</div>
|
|
272
|
+
)}
|
|
273
|
+
|
|
274
|
+
<button
|
|
275
|
+
className="agent-picker-item"
|
|
276
|
+
onClick={() => connect({ kind: 'no-tmux' })}
|
|
277
|
+
>
|
|
278
|
+
<span className="agent-picker-name">Run without tmux</span>
|
|
279
|
+
<span className="agent-picker-desc">{agentLabel} directly — won't survive a dropped connection</span>
|
|
280
|
+
</button>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<button className="remote-link" onClick={() => setStep('hosts')}>← choose another host</button>
|
|
284
|
+
</>
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
{step === 'error' && (
|
|
288
|
+
<>
|
|
289
|
+
<div className="remote-error">Couldn't connect to <b>{target}</b></div>
|
|
290
|
+
<div className="remote-status-sub">{errorMsg}</div>
|
|
291
|
+
{passwordField}
|
|
292
|
+
<div className="remote-section-label">TRY ANYWAY (INTERACTIVE)</div>
|
|
293
|
+
<div className="agent-picker-list">
|
|
294
|
+
<button
|
|
295
|
+
className="agent-picker-item"
|
|
296
|
+
onClick={() => connect({ kind: 'auto', session: `${TMUX_PREFIX}1` })}
|
|
297
|
+
>
|
|
298
|
+
<span className="agent-picker-name">Connect with tmux</span>
|
|
299
|
+
<span className="agent-picker-desc">ssh will prompt if a password / key passphrase is needed</span>
|
|
300
|
+
</button>
|
|
301
|
+
<button
|
|
302
|
+
className="agent-picker-item"
|
|
303
|
+
onClick={() => connect({ kind: 'no-tmux' })}
|
|
304
|
+
>
|
|
305
|
+
<span className="agent-picker-name">Connect without tmux</span>
|
|
306
|
+
<span className="agent-picker-desc">run {agentLabel} directly</span>
|
|
307
|
+
</button>
|
|
308
|
+
</div>
|
|
309
|
+
<button className="remote-link" onClick={() => setStep('hosts')}>← choose another host</button>
|
|
310
|
+
</>
|
|
311
|
+
)}
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
)
|
|
315
|
+
}
|
package/src/renderer/styles.css
CHANGED
|
@@ -1150,6 +1150,9 @@ html, body, #root {
|
|
|
1150
1150
|
border-radius: 12px;
|
|
1151
1151
|
padding: 32px 40px;
|
|
1152
1152
|
min-width: 360px;
|
|
1153
|
+
max-width: 460px;
|
|
1154
|
+
max-height: 88vh;
|
|
1155
|
+
overflow-y: auto;
|
|
1153
1156
|
text-align: center;
|
|
1154
1157
|
}
|
|
1155
1158
|
|
|
@@ -1218,6 +1221,7 @@ html, body, #root {
|
|
|
1218
1221
|
display: flex;
|
|
1219
1222
|
align-items: center;
|
|
1220
1223
|
justify-content: space-between;
|
|
1224
|
+
gap: 16px;
|
|
1221
1225
|
padding: 14px 18px;
|
|
1222
1226
|
background: #16161e;
|
|
1223
1227
|
border: 1px solid #2a2b3d;
|
|
@@ -1236,11 +1240,13 @@ html, body, #root {
|
|
|
1236
1240
|
font-size: 14px;
|
|
1237
1241
|
font-weight: 600;
|
|
1238
1242
|
color: #c0caf5;
|
|
1243
|
+
flex-shrink: 0;
|
|
1239
1244
|
}
|
|
1240
1245
|
|
|
1241
1246
|
.agent-picker-desc {
|
|
1242
1247
|
font-size: 11px;
|
|
1243
1248
|
color: #565f89;
|
|
1249
|
+
text-align: right;
|
|
1244
1250
|
}
|
|
1245
1251
|
|
|
1246
1252
|
.agent-picker-hint {
|
|
@@ -1265,6 +1271,201 @@ html, body, #root {
|
|
|
1265
1271
|
accent-color: #7aa2f7;
|
|
1266
1272
|
}
|
|
1267
1273
|
|
|
1274
|
+
/* ─── Remote (SSH) connect ───────────────────────── */
|
|
1275
|
+
|
|
1276
|
+
.remote-section-label {
|
|
1277
|
+
font-size: 10px;
|
|
1278
|
+
color: #565f89;
|
|
1279
|
+
text-transform: uppercase;
|
|
1280
|
+
letter-spacing: 1px;
|
|
1281
|
+
text-align: left;
|
|
1282
|
+
margin: 16px 0 8px;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
/* Scroll the host list rather than letting it grow the whole modal. */
|
|
1286
|
+
.remote-host-scroll {
|
|
1287
|
+
max-height: 40vh;
|
|
1288
|
+
overflow-y: auto;
|
|
1289
|
+
padding-right: 4px;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/* Project rows: title on top, path muted + truncated below (paths are long). */
|
|
1293
|
+
.project-item {
|
|
1294
|
+
flex-direction: column;
|
|
1295
|
+
align-items: flex-start;
|
|
1296
|
+
gap: 4px;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.project-item .agent-picker-name {
|
|
1300
|
+
max-width: 100%;
|
|
1301
|
+
overflow: hidden;
|
|
1302
|
+
text-overflow: ellipsis;
|
|
1303
|
+
white-space: nowrap;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
.project-item .agent-picker-desc {
|
|
1307
|
+
text-align: left;
|
|
1308
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
1309
|
+
font-size: 10px;
|
|
1310
|
+
max-width: 100%;
|
|
1311
|
+
overflow: hidden;
|
|
1312
|
+
text-overflow: ellipsis;
|
|
1313
|
+
white-space: nowrap;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/* Compact host rows so more fit before scrolling. */
|
|
1317
|
+
.remote-host-scroll .agent-picker-item {
|
|
1318
|
+
padding: 10px 14px;
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
.remote-toggle {
|
|
1322
|
+
display: flex;
|
|
1323
|
+
align-items: center;
|
|
1324
|
+
gap: 8px;
|
|
1325
|
+
margin-bottom: 4px;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
.remote-toggle-label {
|
|
1329
|
+
font-size: 12px;
|
|
1330
|
+
color: #565f89;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
.remote-toggle-btn {
|
|
1334
|
+
flex: 1;
|
|
1335
|
+
padding: 8px 10px;
|
|
1336
|
+
background: #16161e;
|
|
1337
|
+
border: 1px solid #2a2b3d;
|
|
1338
|
+
border-radius: 8px;
|
|
1339
|
+
color: #a9b1d6;
|
|
1340
|
+
font-size: 12px;
|
|
1341
|
+
cursor: pointer;
|
|
1342
|
+
transition: all 0.15s;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
.remote-toggle-btn:hover {
|
|
1346
|
+
border-color: #7aa2f7;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
.remote-toggle-btn.is-active {
|
|
1350
|
+
border-color: #7aa2f7;
|
|
1351
|
+
background: #1a1b2e;
|
|
1352
|
+
color: #c0caf5;
|
|
1353
|
+
font-weight: 600;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
.remote-manual {
|
|
1357
|
+
display: flex;
|
|
1358
|
+
gap: 8px;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
.remote-input {
|
|
1362
|
+
flex: 1;
|
|
1363
|
+
padding: 10px 12px;
|
|
1364
|
+
background: #16161e;
|
|
1365
|
+
border: 1px solid #2a2b3d;
|
|
1366
|
+
border-radius: 8px;
|
|
1367
|
+
color: #c0caf5;
|
|
1368
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
1369
|
+
font-size: 12px;
|
|
1370
|
+
outline: none;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
.remote-input:focus {
|
|
1374
|
+
border-color: #7aa2f7;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
.remote-btn {
|
|
1378
|
+
padding: 10px 16px;
|
|
1379
|
+
background: #7aa2f7;
|
|
1380
|
+
border: none;
|
|
1381
|
+
border-radius: 8px;
|
|
1382
|
+
color: #1f2335;
|
|
1383
|
+
font-size: 12px;
|
|
1384
|
+
font-weight: 600;
|
|
1385
|
+
cursor: pointer;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
.remote-btn:disabled {
|
|
1389
|
+
opacity: 0.4;
|
|
1390
|
+
cursor: default;
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
.remote-link {
|
|
1394
|
+
margin-top: 18px;
|
|
1395
|
+
background: none;
|
|
1396
|
+
border: none;
|
|
1397
|
+
color: #7aa2f7;
|
|
1398
|
+
font-size: 12px;
|
|
1399
|
+
cursor: pointer;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
.remote-status {
|
|
1403
|
+
padding: 24px 8px;
|
|
1404
|
+
color: #a9b1d6;
|
|
1405
|
+
font-size: 13px;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
.remote-status-sub {
|
|
1409
|
+
margin-top: 6px;
|
|
1410
|
+
font-size: 11px;
|
|
1411
|
+
color: #565f89;
|
|
1412
|
+
word-break: break-word;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
.remote-spinner {
|
|
1416
|
+
width: 22px;
|
|
1417
|
+
height: 22px;
|
|
1418
|
+
margin: 0 auto 14px;
|
|
1419
|
+
border: 2px solid #2a2b3d;
|
|
1420
|
+
border-top-color: #7aa2f7;
|
|
1421
|
+
border-radius: 50%;
|
|
1422
|
+
animation: remote-spin 0.8s linear infinite;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
@keyframes remote-spin {
|
|
1426
|
+
to { transform: rotate(360deg); }
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
.remote-target-line {
|
|
1430
|
+
font-size: 13px;
|
|
1431
|
+
color: #a9b1d6;
|
|
1432
|
+
margin-bottom: 4px;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
.remote-note {
|
|
1436
|
+
font-size: 11px;
|
|
1437
|
+
color: #e0af68;
|
|
1438
|
+
background: rgba(224, 175, 104, 0.08);
|
|
1439
|
+
border: 1px solid rgba(224, 175, 104, 0.25);
|
|
1440
|
+
border-radius: 8px;
|
|
1441
|
+
padding: 8px 12px;
|
|
1442
|
+
margin: 8px 0;
|
|
1443
|
+
text-align: left;
|
|
1444
|
+
line-height: 1.4;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
.remote-error {
|
|
1448
|
+
font-size: 13px;
|
|
1449
|
+
color: #f7768e;
|
|
1450
|
+
margin-bottom: 4px;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
.remote-pw {
|
|
1454
|
+
margin: 8px 0 4px;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
.remote-pw .remote-input {
|
|
1458
|
+
width: 100%;
|
|
1459
|
+
box-sizing: border-box;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
.remote-pw-hint {
|
|
1463
|
+
margin-top: 6px;
|
|
1464
|
+
font-size: 10px;
|
|
1465
|
+
color: #565f89;
|
|
1466
|
+
text-align: left;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1268
1469
|
/* Settings dialog */
|
|
1269
1470
|
.settings-overlay {
|
|
1270
1471
|
position: fixed;
|