@phenx-inc/ctlsurf 0.1.2 → 0.1.4
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 +37 -78
- package/out/headless/index.mjs.map +2 -2
- package/out/main/index.js +3743 -112
- package/out/renderer/assets/{cssMode-DL0XItGB.js → cssMode-CY6x0qXW.js} +3 -3
- package/out/renderer/assets/{freemarker2-CrOEuDcF.js → freemarker2-BXSW9BAX.js} +1 -1
- package/out/renderer/assets/{handlebars-D4QYaBof.js → handlebars-BYUZ1IOs.js} +1 -1
- package/out/renderer/assets/{html-B2Dqk2ai.js → html-DPocQM4t.js} +1 -1
- package/out/renderer/assets/{htmlMode-CdZ0Prhd.js → htmlMode-CsPinKYA.js} +3 -3
- package/out/renderer/assets/{index-pZmE1QXB.js → index-Bml7oDn9.js} +84 -36
- package/out/renderer/assets/{index-CJ6RsQWP.css → index-DK9wLFFm.css} +146 -0
- package/out/renderer/assets/{javascript-CK8zNQXj.js → javascript-_HVGB-lj.js} +2 -2
- package/out/renderer/assets/{jsonMode-Cewaellc.js → jsonMode-JbrRQBOU.js} +3 -3
- package/out/renderer/assets/{liquid-Bd3GPNs2.js → liquid-B7izKdqo.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-DSDH7BnA.js → lspLanguageFeatures-DzxH499X.js} +1 -1
- package/out/renderer/assets/{mdx-CCPVCrXC.js → mdx-CmvUeYLw.js} +1 -1
- package/out/renderer/assets/{python-34jOtlcC.js → python-DJqYTFoi.js} +1 -1
- package/out/renderer/assets/{razor-DXRw694z.js → razor-CGEA5nUK.js} +1 -1
- package/out/renderer/assets/{tsMode-CmND5_wB.js → tsMode-CN0FOHMy.js} +1 -1
- package/out/renderer/assets/{typescript-BNNI0Euv.js → typescript-CIn-DSfY.js} +1 -1
- package/out/renderer/assets/{xml-CgdndrNB.js → xml-C5t3U2jS.js} +1 -1
- package/out/renderer/assets/{yaml-DNWPIf1s.js → yaml-n-Jb6xf1.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +6 -4
- package/src/main/bridge.ts +25 -74
- package/src/main/orchestrator.ts +9 -11
- package/src/main/workerWs.ts +10 -2
- package/src/renderer/App.tsx +38 -12
- package/src/renderer/components/AgentPicker.tsx +49 -0
- package/src/renderer/styles.css +146 -0
package/src/main/bridge.ts
CHANGED
|
@@ -1,66 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { WorkerWsClient } from './workerWs'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Conversation Bridge
|
|
5
5
|
*
|
|
6
|
-
* Taps the pty output stream
|
|
7
|
-
*
|
|
6
|
+
* Taps the pty output stream, strips ANSI codes, buffers output,
|
|
7
|
+
* and sends cleaned chunks to the backend via WebSocket for S3-backed chat logging.
|
|
8
8
|
*/
|
|
9
9
|
export class ConversationBridge {
|
|
10
|
-
private
|
|
11
|
-
private logBlockId: string | null = null
|
|
12
|
-
private pageId: string | null = null
|
|
10
|
+
private wsClient: WorkerWsClient | null = null
|
|
13
11
|
private buffer: string = ''
|
|
14
12
|
private flushTimer: ReturnType<typeof setTimeout> | null = null
|
|
15
13
|
private flushIntervalMs: number = 3000 // flush every 3 seconds
|
|
16
|
-
private agentName: string = 'shell'
|
|
17
14
|
private sessionActive: boolean = false
|
|
18
15
|
private inputBuffer: string = ''
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
this.
|
|
17
|
+
setWsClient(ws: WorkerWsClient): void {
|
|
18
|
+
this.wsClient = ws
|
|
22
19
|
}
|
|
23
20
|
|
|
24
21
|
/**
|
|
25
22
|
* Start a new logging session.
|
|
26
|
-
* Creates a log block on the given dataspace page.
|
|
27
23
|
*/
|
|
28
|
-
|
|
29
|
-
if (!this.api.getApiKey()) {
|
|
30
|
-
console.log('[bridge] No API key set, skipping session logging')
|
|
31
|
-
return
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
this.pageId = dataspacePageId
|
|
35
|
-
this.agentName = agentName
|
|
24
|
+
startSession(): void {
|
|
36
25
|
this.buffer = ''
|
|
37
26
|
this.inputBuffer = ''
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19)
|
|
41
|
-
const block = await this.api.createBlock(dataspacePageId, {
|
|
42
|
-
type: 'log',
|
|
43
|
-
title: `${agentName} — ${timestamp} — ${cwd}`,
|
|
44
|
-
props: {
|
|
45
|
-
entries: [],
|
|
46
|
-
max_entries: 1000
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
this.logBlockId = block.id
|
|
50
|
-
this.sessionActive = true
|
|
51
|
-
|
|
52
|
-
// Log session start
|
|
53
|
-
await this.api.appendLog(this.logBlockId, 'session_start', `Started ${agentName} session`, {
|
|
54
|
-
agent: agentName,
|
|
55
|
-
cwd,
|
|
56
|
-
timestamp
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
console.log(`[bridge] Session started, log block: ${this.logBlockId}`)
|
|
60
|
-
} catch (err: any) {
|
|
61
|
-
console.error(`[bridge] Failed to start session:`, err.message)
|
|
62
|
-
this.sessionActive = false
|
|
63
|
-
}
|
|
27
|
+
this.sessionActive = true
|
|
28
|
+
console.log('[bridge] Session started')
|
|
64
29
|
}
|
|
65
30
|
|
|
66
31
|
/**
|
|
@@ -90,17 +55,17 @@ export class ConversationBridge {
|
|
|
90
55
|
if (data.includes('\r') || data.includes('\n')) {
|
|
91
56
|
const input = this.inputBuffer.trim()
|
|
92
57
|
if (input.length > 0) {
|
|
93
|
-
this.
|
|
58
|
+
this.sendEntry('user_input', input)
|
|
94
59
|
}
|
|
95
60
|
this.inputBuffer = ''
|
|
96
61
|
}
|
|
97
62
|
}
|
|
98
63
|
|
|
99
64
|
/**
|
|
100
|
-
* Flush buffered output
|
|
65
|
+
* Flush buffered output via WebSocket.
|
|
101
66
|
*/
|
|
102
|
-
private
|
|
103
|
-
if (
|
|
67
|
+
private flush(): void {
|
|
68
|
+
if (this.buffer.length === 0) return
|
|
104
69
|
|
|
105
70
|
const chunk = this.buffer
|
|
106
71
|
this.buffer = ''
|
|
@@ -109,42 +74,29 @@ export class ConversationBridge {
|
|
|
109
74
|
const cleaned = stripAnsi(chunk)
|
|
110
75
|
if (cleaned.trim().length === 0) return
|
|
111
76
|
|
|
112
|
-
|
|
113
|
-
await this.api.appendLog(this.logBlockId, 'terminal_output', cleaned)
|
|
114
|
-
} catch (err: any) {
|
|
115
|
-
console.error(`[bridge] Failed to append log:`, err.message)
|
|
116
|
-
}
|
|
77
|
+
this.sendEntry('terminal_output', cleaned)
|
|
117
78
|
}
|
|
118
79
|
|
|
119
80
|
/**
|
|
120
|
-
*
|
|
81
|
+
* Send an entry to the backend via WebSocket.
|
|
121
82
|
*/
|
|
122
|
-
private
|
|
123
|
-
if (!this.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
83
|
+
private sendEntry(type: string, content: string): void {
|
|
84
|
+
if (!this.wsClient) return
|
|
85
|
+
this.wsClient.sendChatLog({
|
|
86
|
+
ts: new Date().toISOString(),
|
|
87
|
+
type,
|
|
88
|
+
content,
|
|
89
|
+
})
|
|
129
90
|
}
|
|
130
91
|
|
|
131
92
|
/**
|
|
132
93
|
* End the current session.
|
|
133
94
|
*/
|
|
134
|
-
|
|
135
|
-
if (!this.sessionActive
|
|
95
|
+
endSession(): void {
|
|
96
|
+
if (!this.sessionActive) return
|
|
136
97
|
|
|
137
98
|
// Flush remaining buffer
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
await this.api.appendLog(this.logBlockId, 'session_end', `Session ended (exit code: ${exitCode ?? 'unknown'})`, {
|
|
142
|
-
agent: this.agentName,
|
|
143
|
-
exitCode
|
|
144
|
-
})
|
|
145
|
-
} catch (err: any) {
|
|
146
|
-
console.error(`[bridge] Failed to log session end:`, err.message)
|
|
147
|
-
}
|
|
99
|
+
this.flush()
|
|
148
100
|
|
|
149
101
|
if (this.flushTimer) {
|
|
150
102
|
clearTimeout(this.flushTimer)
|
|
@@ -152,7 +104,6 @@ export class ConversationBridge {
|
|
|
152
104
|
}
|
|
153
105
|
|
|
154
106
|
this.sessionActive = false
|
|
155
|
-
this.logBlockId = null
|
|
156
107
|
this.buffer = ''
|
|
157
108
|
this.inputBuffer = ''
|
|
158
109
|
console.log('[bridge] Session ended')
|
package/src/main/orchestrator.ts
CHANGED
|
@@ -57,7 +57,7 @@ export class Orchestrator {
|
|
|
57
57
|
|
|
58
58
|
// Core services
|
|
59
59
|
readonly ctlsurfApi = new CtlsurfApi()
|
|
60
|
-
readonly bridge = new ConversationBridge(
|
|
60
|
+
readonly bridge = new ConversationBridge()
|
|
61
61
|
readonly workerWs: WorkerWsClient
|
|
62
62
|
|
|
63
63
|
// State
|
|
@@ -105,6 +105,8 @@ export class Orchestrator {
|
|
|
105
105
|
this.ptyManager?.write(data)
|
|
106
106
|
},
|
|
107
107
|
})
|
|
108
|
+
|
|
109
|
+
this.bridge.setWsClient(this.workerWs)
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
// ─── Settings ───────────────────────────────────
|
|
@@ -282,7 +284,7 @@ export class Orchestrator {
|
|
|
282
284
|
|
|
283
285
|
async spawnAgent(agent: AgentConfig, cwd: string): Promise<void> {
|
|
284
286
|
if (this.ptyManager) {
|
|
285
|
-
|
|
287
|
+
this.bridge.endSession()
|
|
286
288
|
this.ptyManager.kill()
|
|
287
289
|
}
|
|
288
290
|
|
|
@@ -303,19 +305,15 @@ export class Orchestrator {
|
|
|
303
305
|
|
|
304
306
|
const thisPtyManager = this.ptyManager
|
|
305
307
|
|
|
306
|
-
this.ptyManager.onExit(
|
|
308
|
+
this.ptyManager.onExit((exitCode: number) => {
|
|
307
309
|
this.events.onPtyExit(exitCode)
|
|
308
|
-
|
|
310
|
+
this.bridge.endSession()
|
|
309
311
|
if (thisPtyManager === this.ptyManager && this.currentAgent && isCodingAgent(this.currentAgent)) {
|
|
310
312
|
this.workerWs.disconnect()
|
|
311
313
|
}
|
|
312
314
|
})
|
|
313
315
|
|
|
314
|
-
|
|
315
|
-
const dataspacePageId = profile.dataspacePageId || process.env.CTLSURF_DATASPACE_PAGE_ID || ''
|
|
316
|
-
if (dataspacePageId && this.ctlsurfApi.getApiKey()) {
|
|
317
|
-
await this.bridge.startSession(dataspacePageId, agent.name, cwd)
|
|
318
|
-
}
|
|
316
|
+
this.bridge.startSession()
|
|
319
317
|
|
|
320
318
|
if (isCodingAgent(agent)) {
|
|
321
319
|
this.connectWorkerWs(agent, cwd)
|
|
@@ -336,7 +334,7 @@ export class Orchestrator {
|
|
|
336
334
|
}
|
|
337
335
|
|
|
338
336
|
async killAgent(): Promise<void> {
|
|
339
|
-
|
|
337
|
+
this.bridge.endSession()
|
|
340
338
|
this.ptyManager?.kill()
|
|
341
339
|
this.ptyManager = null
|
|
342
340
|
if (this.currentAgent && isCodingAgent(this.currentAgent)) {
|
|
@@ -392,7 +390,7 @@ export class Orchestrator {
|
|
|
392
390
|
// ─── Shutdown ───────────────────────────────────
|
|
393
391
|
|
|
394
392
|
async shutdown(): Promise<void> {
|
|
395
|
-
|
|
393
|
+
this.bridge.endSession()
|
|
396
394
|
this.ptyManager?.kill()
|
|
397
395
|
this.ptyManager = null
|
|
398
396
|
this.workerWs.disconnect()
|
package/src/main/workerWs.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import os from 'os'
|
|
2
2
|
import crypto from 'crypto'
|
|
3
|
+
import WsModule from 'ws'
|
|
4
|
+
|
|
5
|
+
// Use native WebSocket if available (Node 22+), otherwise fall back to ws package
|
|
6
|
+
const WS: typeof WebSocket = typeof WebSocket !== 'undefined' ? WebSocket : WsModule as any
|
|
3
7
|
|
|
4
8
|
function log(...args: unknown[]): void {
|
|
5
9
|
try { console.log(...args) } catch { /* EPIPE safe */ }
|
|
@@ -130,6 +134,10 @@ export class WorkerWsClient {
|
|
|
130
134
|
this.send({ type: 'terminal_resize', cols, rows })
|
|
131
135
|
}
|
|
132
136
|
|
|
137
|
+
sendChatLog(entry: { type: string; content: string; ts?: string }): void {
|
|
138
|
+
this.send({ type: 'chat_log', entry })
|
|
139
|
+
}
|
|
140
|
+
|
|
133
141
|
private doConnect(): void {
|
|
134
142
|
if (!this.apiKey || !this.registration) {
|
|
135
143
|
log('[worker-ws] No API key or registration, skipping connect')
|
|
@@ -169,7 +177,7 @@ export class WorkerWsClient {
|
|
|
169
177
|
log(`[worker-ws] Connecting to ${url.replace(/token=.*/, 'token=***')}...`)
|
|
170
178
|
|
|
171
179
|
try {
|
|
172
|
-
this.ws = new
|
|
180
|
+
this.ws = new WS(url) as unknown as WebSocket
|
|
173
181
|
} catch (err) {
|
|
174
182
|
log('[worker-ws] Failed to create WebSocket:', err)
|
|
175
183
|
this.scheduleReconnect()
|
|
@@ -272,7 +280,7 @@ export class WorkerWsClient {
|
|
|
272
280
|
}
|
|
273
281
|
|
|
274
282
|
private send(data: Record<string, unknown>): void {
|
|
275
|
-
if (this.ws && this.ws.readyState ===
|
|
283
|
+
if (this.ws && this.ws.readyState === WS.OPEN) {
|
|
276
284
|
this.ws.send(JSON.stringify(data))
|
|
277
285
|
}
|
|
278
286
|
}
|
package/src/renderer/App.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'
|
|
|
2
2
|
import { TerminalPanel } from './components/TerminalPanel'
|
|
3
3
|
import { CtlsurfPanel } from './components/CtlsurfPanel'
|
|
4
4
|
import { EditorPanel } from './components/EditorPanel'
|
|
5
|
+
import { AgentPicker } from './components/AgentPicker'
|
|
5
6
|
import {
|
|
6
7
|
PaneLayout,
|
|
7
8
|
type LayoutNode,
|
|
@@ -63,11 +64,10 @@ const DEFAULT_LAYOUT: LayoutNode = {
|
|
|
63
64
|
type: 'split',
|
|
64
65
|
direction: 'horizontal',
|
|
65
66
|
children: [
|
|
66
|
-
{ type: 'leaf', paneId: 'editor' },
|
|
67
67
|
{ type: 'leaf', paneId: 'terminal' },
|
|
68
68
|
{ type: 'leaf', paneId: 'ctlsurf' },
|
|
69
69
|
],
|
|
70
|
-
sizes: [
|
|
70
|
+
sizes: [50, 50],
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
const ALL_PANE_IDS = ['editor', 'terminal', 'ctlsurf']
|
|
@@ -86,14 +86,18 @@ export default function App() {
|
|
|
86
86
|
// Track which panes are in the layout tree
|
|
87
87
|
const visiblePaneIds = findPaneIds(layout)
|
|
88
88
|
|
|
89
|
+
const [showAgentPicker, setShowAgentPicker] = useState(true)
|
|
90
|
+
|
|
89
91
|
useEffect(() => {
|
|
90
92
|
async function init() {
|
|
91
93
|
const list = await window.worker.listAgents()
|
|
92
|
-
const defaultAgent = await window.worker.getDefaultAgent()
|
|
93
94
|
setAgents(list)
|
|
94
|
-
setSelectedAgent(defaultAgent)
|
|
95
95
|
const status = await window.worker.getWorkerStatus()
|
|
96
96
|
setWsStatus(status)
|
|
97
|
+
// Load initial cwd for the picker
|
|
98
|
+
const initialCwd = await window.worker.getCwd().catch(() => window.worker.getHomePath())
|
|
99
|
+
setCwd(initialCwd)
|
|
100
|
+
cwdRef.current = initialCwd
|
|
97
101
|
}
|
|
98
102
|
init()
|
|
99
103
|
}, [])
|
|
@@ -232,6 +236,18 @@ export default function App() {
|
|
|
232
236
|
<button className="titlebar-btn" onClick={() => setShowSettings(true)} title="Settings">
|
|
233
237
|
Settings
|
|
234
238
|
</button>
|
|
239
|
+
<span className="titlebar-separator" />
|
|
240
|
+
{agents.map(a => (
|
|
241
|
+
<button
|
|
242
|
+
key={a.id}
|
|
243
|
+
className={`titlebar-agent-btn ${selectedAgent?.id === a.id ? 'active' : ''}`}
|
|
244
|
+
onClick={() => handleAgentChange(a.id)}
|
|
245
|
+
title={a.description}
|
|
246
|
+
>
|
|
247
|
+
{a.name}
|
|
248
|
+
</button>
|
|
249
|
+
))}
|
|
250
|
+
<span className="titlebar-separator" />
|
|
235
251
|
{ALL_PANE_IDS.map(id => (
|
|
236
252
|
<button
|
|
237
253
|
key={id}
|
|
@@ -242,14 +258,6 @@ export default function App() {
|
|
|
242
258
|
{id === 'editor' ? 'Editor' : id === 'terminal' ? 'Terminal' : 'ctlsurf'}
|
|
243
259
|
</button>
|
|
244
260
|
))}
|
|
245
|
-
<select
|
|
246
|
-
value={selectedAgent?.id || ''}
|
|
247
|
-
onChange={(e) => handleAgentChange(e.target.value)}
|
|
248
|
-
>
|
|
249
|
-
{agents.map(a => (
|
|
250
|
-
<option key={a.id} value={a.id}>{a.name}</option>
|
|
251
|
-
))}
|
|
252
|
-
</select>
|
|
253
261
|
</div>
|
|
254
262
|
</div>
|
|
255
263
|
|
|
@@ -270,6 +278,24 @@ export default function App() {
|
|
|
270
278
|
|
|
271
279
|
<StatusBar wsStatus={wsStatus} cwd={cwd} onChangeCwd={handleChangeCwd} />
|
|
272
280
|
<SettingsDialog open={showSettings} onClose={() => setShowSettings(false)} />
|
|
281
|
+
|
|
282
|
+
{showAgentPicker && agents.length > 0 && (
|
|
283
|
+
<AgentPicker
|
|
284
|
+
agents={agents}
|
|
285
|
+
cwd={cwd || ''}
|
|
286
|
+
onSelect={(agent) => {
|
|
287
|
+
setSelectedAgent(agent)
|
|
288
|
+
setShowAgentPicker(false)
|
|
289
|
+
}}
|
|
290
|
+
onChangeCwd={async () => {
|
|
291
|
+
const newCwd = await window.worker.browseCwd()
|
|
292
|
+
if (newCwd) {
|
|
293
|
+
setCwd(newCwd)
|
|
294
|
+
cwdRef.current = newCwd
|
|
295
|
+
}
|
|
296
|
+
}}
|
|
297
|
+
/>
|
|
298
|
+
)}
|
|
273
299
|
</div>
|
|
274
300
|
)
|
|
275
301
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
interface AgentConfig {
|
|
2
|
+
id: string
|
|
3
|
+
name: string
|
|
4
|
+
command: string
|
|
5
|
+
args: string[]
|
|
6
|
+
description: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface AgentPickerProps {
|
|
10
|
+
agents: AgentConfig[]
|
|
11
|
+
cwd: string
|
|
12
|
+
onSelect: (agent: AgentConfig) => void
|
|
13
|
+
onChangeCwd: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AgentPicker({ agents, cwd, onSelect, onChangeCwd }: AgentPickerProps) {
|
|
17
|
+
const home = typeof window !== 'undefined' ? '' : ''
|
|
18
|
+
// Shorten home dir for display
|
|
19
|
+
const displayPath = cwd.replace(/^\/Users\/[^/]+/, '~')
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="agent-picker-overlay">
|
|
23
|
+
<div className="agent-picker-modal">
|
|
24
|
+
<div className="agent-picker-brand">ctlsurf</div>
|
|
25
|
+
|
|
26
|
+
<div className="agent-picker-cwd" onClick={onChangeCwd} title="Click to change directory">
|
|
27
|
+
<span className="agent-picker-cwd-icon">📂</span>
|
|
28
|
+
<span className="agent-picker-cwd-path">{displayPath}</span>
|
|
29
|
+
<span className="agent-picker-cwd-change">change</span>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div className="agent-picker-title">SELECT AGENT</div>
|
|
33
|
+
<div className="agent-picker-list">
|
|
34
|
+
{agents.map(a => (
|
|
35
|
+
<button
|
|
36
|
+
key={a.id}
|
|
37
|
+
className="agent-picker-item"
|
|
38
|
+
onClick={() => onSelect(a)}
|
|
39
|
+
>
|
|
40
|
+
<span className="agent-picker-name">{a.name}</span>
|
|
41
|
+
<span className="agent-picker-desc">{a.description}</span>
|
|
42
|
+
</button>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
<div className="agent-picker-hint">Select an agent to start</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
package/src/renderer/styles.css
CHANGED
|
@@ -82,6 +82,36 @@ html, body, #root {
|
|
|
82
82
|
background: #1f2335;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
.titlebar-separator {
|
|
86
|
+
width: 1px;
|
|
87
|
+
height: 16px;
|
|
88
|
+
background: #3b3d57;
|
|
89
|
+
margin: 0 4px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.titlebar-agent-btn {
|
|
93
|
+
background: #2a2b3d;
|
|
94
|
+
color: #565f89;
|
|
95
|
+
border: 1px solid #3b3d57;
|
|
96
|
+
border-radius: 4px;
|
|
97
|
+
padding: 2px 12px;
|
|
98
|
+
font-size: 11px;
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
transition: all 0.15s;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.titlebar-agent-btn:hover {
|
|
104
|
+
color: #a9b1d6;
|
|
105
|
+
border-color: #565f89;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.titlebar-agent-btn.active {
|
|
109
|
+
color: #1a1b26;
|
|
110
|
+
background: #7aa2f7;
|
|
111
|
+
border-color: #7aa2f7;
|
|
112
|
+
font-weight: 600;
|
|
113
|
+
}
|
|
114
|
+
|
|
85
115
|
/* Main content area */
|
|
86
116
|
.main-content {
|
|
87
117
|
flex: 1;
|
|
@@ -530,6 +560,122 @@ html, body, #root {
|
|
|
530
560
|
font-size: 13px;
|
|
531
561
|
}
|
|
532
562
|
|
|
563
|
+
/* Agent picker modal */
|
|
564
|
+
.agent-picker-overlay {
|
|
565
|
+
position: fixed;
|
|
566
|
+
inset: 0;
|
|
567
|
+
background: rgba(0, 0, 0, 0.7);
|
|
568
|
+
display: flex;
|
|
569
|
+
align-items: center;
|
|
570
|
+
justify-content: center;
|
|
571
|
+
z-index: 200;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.agent-picker-modal {
|
|
575
|
+
background: #1f2335;
|
|
576
|
+
border: 1px solid #3b3d57;
|
|
577
|
+
border-radius: 12px;
|
|
578
|
+
padding: 32px 40px;
|
|
579
|
+
min-width: 360px;
|
|
580
|
+
text-align: center;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.agent-picker-brand {
|
|
584
|
+
font-size: 22px;
|
|
585
|
+
font-weight: 700;
|
|
586
|
+
color: #7aa2f7;
|
|
587
|
+
margin-bottom: 20px;
|
|
588
|
+
letter-spacing: 0.5px;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.agent-picker-cwd {
|
|
592
|
+
display: flex;
|
|
593
|
+
align-items: center;
|
|
594
|
+
justify-content: center;
|
|
595
|
+
gap: 8px;
|
|
596
|
+
padding: 10px 16px;
|
|
597
|
+
margin-bottom: 20px;
|
|
598
|
+
background: #16161e;
|
|
599
|
+
border: 1px solid #2a2b3d;
|
|
600
|
+
border-radius: 8px;
|
|
601
|
+
cursor: pointer;
|
|
602
|
+
transition: border-color 0.15s;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.agent-picker-cwd:hover {
|
|
606
|
+
border-color: #7aa2f7;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.agent-picker-cwd-icon {
|
|
610
|
+
font-size: 14px;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.agent-picker-cwd-path {
|
|
614
|
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
|
615
|
+
font-size: 12px;
|
|
616
|
+
color: #a9b1d6;
|
|
617
|
+
flex: 1;
|
|
618
|
+
text-align: left;
|
|
619
|
+
overflow: hidden;
|
|
620
|
+
text-overflow: ellipsis;
|
|
621
|
+
white-space: nowrap;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
.agent-picker-cwd-change {
|
|
625
|
+
font-size: 11px;
|
|
626
|
+
color: #7aa2f7;
|
|
627
|
+
flex-shrink: 0;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.agent-picker-title {
|
|
631
|
+
font-size: 11px;
|
|
632
|
+
color: #565f89;
|
|
633
|
+
margin-bottom: 12px;
|
|
634
|
+
text-transform: uppercase;
|
|
635
|
+
letter-spacing: 1px;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.agent-picker-list {
|
|
639
|
+
display: flex;
|
|
640
|
+
flex-direction: column;
|
|
641
|
+
gap: 8px;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.agent-picker-item {
|
|
645
|
+
display: flex;
|
|
646
|
+
align-items: center;
|
|
647
|
+
justify-content: space-between;
|
|
648
|
+
padding: 14px 18px;
|
|
649
|
+
background: #16161e;
|
|
650
|
+
border: 1px solid #2a2b3d;
|
|
651
|
+
border-radius: 8px;
|
|
652
|
+
cursor: pointer;
|
|
653
|
+
transition: all 0.15s;
|
|
654
|
+
text-align: left;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.agent-picker-item:hover {
|
|
658
|
+
border-color: #7aa2f7;
|
|
659
|
+
background: #1a1b2e;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.agent-picker-name {
|
|
663
|
+
font-size: 14px;
|
|
664
|
+
font-weight: 600;
|
|
665
|
+
color: #c0caf5;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.agent-picker-desc {
|
|
669
|
+
font-size: 11px;
|
|
670
|
+
color: #565f89;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.agent-picker-hint {
|
|
674
|
+
margin-top: 20px;
|
|
675
|
+
font-size: 11px;
|
|
676
|
+
color: #414868;
|
|
677
|
+
}
|
|
678
|
+
|
|
533
679
|
/* Settings dialog */
|
|
534
680
|
.settings-overlay {
|
|
535
681
|
position: fixed;
|