@phenx-inc/ctlsurf 0.1.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/bin/ctlsurf-worker.js +173 -0
- package/electron-vite.config.ts +34 -0
- package/out/headless/index.mjs +1364 -0
- package/out/headless/index.mjs.map +7 -0
- package/out/main/index.js +1131 -0
- package/out/preload/index.js +67 -0
- package/out/renderer/assets/abap-D5KwWAsZ.js +1399 -0
- package/out/renderer/assets/apex-DVGUZ64i.js +331 -0
- package/out/renderer/assets/azcli-BEAhqcuE.js +69 -0
- package/out/renderer/assets/bat-Bqkp9Cfu.js +101 -0
- package/out/renderer/assets/bicep-DIlfshcM.js +110 -0
- package/out/renderer/assets/cameligo-CLaaYNMV.js +175 -0
- package/out/renderer/assets/clojure-fcgFaMHx.js +762 -0
- package/out/renderer/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/out/renderer/assets/coffee-CzJ5oEdj.js +233 -0
- package/out/renderer/assets/cpp-CcN6f0ik.js +390 -0
- package/out/renderer/assets/csharp-BJeIuvde.js +327 -0
- package/out/renderer/assets/csp-D_3BK2Wp.js +54 -0
- package/out/renderer/assets/css-i3rI3_64.js +186 -0
- package/out/renderer/assets/css.worker-umuuUiIb.js +53567 -0
- package/out/renderer/assets/cssMode-DL0XItGB.js +208 -0
- package/out/renderer/assets/cypher-D0--_GAN.js +264 -0
- package/out/renderer/assets/dart-vLMHv35g.js +282 -0
- package/out/renderer/assets/dockerfile--oxj0cAH.js +131 -0
- package/out/renderer/assets/ecl-CeuUgzaZ.js +457 -0
- package/out/renderer/assets/editor.worker-CNgWLVu7.js +13695 -0
- package/out/renderer/assets/elixir-eLfY1jWH.js +570 -0
- package/out/renderer/assets/flow9-ZSTChSMd.js +143 -0
- package/out/renderer/assets/freemarker2-CrOEuDcF.js +995 -0
- package/out/renderer/assets/fsharp-D2uoxuLH.js +218 -0
- package/out/renderer/assets/go-brnMpFrj.js +219 -0
- package/out/renderer/assets/graphql-BeiGgjIU.js +152 -0
- package/out/renderer/assets/handlebars-D4QYaBof.js +414 -0
- package/out/renderer/assets/hcl-CrX1Es2W.js +184 -0
- package/out/renderer/assets/html-B2Dqk2ai.js +303 -0
- package/out/renderer/assets/html.worker-BT47iy49.js +29777 -0
- package/out/renderer/assets/htmlMode-CdZ0Prhd.js +224 -0
- package/out/renderer/assets/index-CJ6RsQWP.css +8108 -0
- package/out/renderer/assets/index-pZmE1QXB.js +211777 -0
- package/out/renderer/assets/ini-BcQysCTb.js +72 -0
- package/out/renderer/assets/java-Dt3iMn2o.js +233 -0
- package/out/renderer/assets/javascript-CK8zNQXj.js +72 -0
- package/out/renderer/assets/json.worker-D4JVmXIe.js +21424 -0
- package/out/renderer/assets/jsonMode-Cewaellc.js +931 -0
- package/out/renderer/assets/julia-Cm3ItYL_.js +512 -0
- package/out/renderer/assets/kotlin-Ddo1SjA5.js +253 -0
- package/out/renderer/assets/less-B7Qaxw-O.js +162 -0
- package/out/renderer/assets/lexon-C1U0m2n9.js +158 -0
- package/out/renderer/assets/liquid-Bd3GPNs2.js +235 -0
- package/out/renderer/assets/lspLanguageFeatures-DSDH7BnA.js +1841 -0
- package/out/renderer/assets/lua-hNsuGJkO.js +163 -0
- package/out/renderer/assets/m3-6ko6q9-_.js +211 -0
- package/out/renderer/assets/markdown-B0YTnTxW.js +230 -0
- package/out/renderer/assets/mdx-CCPVCrXC.js +159 -0
- package/out/renderer/assets/mips-CJm71dS3.js +199 -0
- package/out/renderer/assets/msdax-BBeIktCY.js +376 -0
- package/out/renderer/assets/mysql-BWiizXSn.js +879 -0
- package/out/renderer/assets/objective-c-B1L1C5EC.js +184 -0
- package/out/renderer/assets/pascal-DMQyD4Xk.js +252 -0
- package/out/renderer/assets/pascaligo-VA_LQ1oU.js +165 -0
- package/out/renderer/assets/perl-DC0Z0tlO.js +627 -0
- package/out/renderer/assets/pgsql-DaSGFTLp.js +852 -0
- package/out/renderer/assets/php-Bkx1qpkQ.js +501 -0
- package/out/renderer/assets/pla-DEV89yYj.js +138 -0
- package/out/renderer/assets/postiats-CVVurEnu.js +908 -0
- package/out/renderer/assets/powerquery-BQ_t1ZiQ.js +891 -0
- package/out/renderer/assets/powershell-BXiKvz7Z.js +240 -0
- package/out/renderer/assets/protobuf-CndvAUGu.js +421 -0
- package/out/renderer/assets/pug-BxCXwerb.js +403 -0
- package/out/renderer/assets/python-34jOtlcC.js +295 -0
- package/out/renderer/assets/qsharp-BWK6YLKm.js +302 -0
- package/out/renderer/assets/r-CtqYUQ6l.js +244 -0
- package/out/renderer/assets/razor-DXRw694z.js +545 -0
- package/out/renderer/assets/redis-O7gSt3oh.js +303 -0
- package/out/renderer/assets/redshift-CvYMMYZY.js +810 -0
- package/out/renderer/assets/restructuredtext-B-KQCVu_.js +175 -0
- package/out/renderer/assets/ruby-DCd4DmAr.js +512 -0
- package/out/renderer/assets/rust-B1c0VCeq.js +344 -0
- package/out/renderer/assets/sb-Chfc_wZF.js +116 -0
- package/out/renderer/assets/scala-DbVzH-3O.js +371 -0
- package/out/renderer/assets/scheme-D7PxodDG.js +109 -0
- package/out/renderer/assets/scss-B42qMyAu.js +261 -0
- package/out/renderer/assets/shell-vZEubQ82.js +222 -0
- package/out/renderer/assets/solidity-yHOxYChb.js +1368 -0
- package/out/renderer/assets/sophia-D7pU0Y1d.js +200 -0
- package/out/renderer/assets/sparql-DxuVdnRl.js +202 -0
- package/out/renderer/assets/sql-BAGepFCR.js +854 -0
- package/out/renderer/assets/st-C-b0Dh53.js +417 -0
- package/out/renderer/assets/swift-BmOZGynf.js +313 -0
- package/out/renderer/assets/systemverilog-BOC0OOdC.js +577 -0
- package/out/renderer/assets/tcl-Bb4GCwBr.js +233 -0
- package/out/renderer/assets/ts.worker-C7hW3aY-.js +225330 -0
- package/out/renderer/assets/tsMode-CmND5_wB.js +1265 -0
- package/out/renderer/assets/twig-DvgEGWAV.js +393 -0
- package/out/renderer/assets/typescript-BNNI0Euv.js +337 -0
- package/out/renderer/assets/typespec-R77Ln7Jb.js +128 -0
- package/out/renderer/assets/vb-Bm6ESA0Q.js +373 -0
- package/out/renderer/assets/wgsl-_KPae5vw.js +454 -0
- package/out/renderer/assets/xml-CgdndrNB.js +89 -0
- package/out/renderer/assets/yaml-DNWPIf1s.js +200 -0
- package/out/renderer/index.html +13 -0
- package/package.json +67 -0
- package/resources/icon.icns +0 -0
- package/resources/icon.ico +0 -0
- package/resources/icon.png +0 -0
- package/src/main/agents.ts +46 -0
- package/src/main/bridge.ts +180 -0
- package/src/main/ctlsurfApi.ts +142 -0
- package/src/main/detectMode.ts +17 -0
- package/src/main/headless.ts +182 -0
- package/src/main/index.ts +300 -0
- package/src/main/orchestrator.ts +404 -0
- package/src/main/pty.ts +65 -0
- package/src/main/settingsDir.ts +17 -0
- package/src/main/tui.ts +366 -0
- package/src/main/workerWs.ts +312 -0
- package/src/preload/index.ts +114 -0
- package/src/renderer/App.tsx +275 -0
- package/src/renderer/components/CtlsurfPanel.tsx +49 -0
- package/src/renderer/components/EditorPanel.tsx +232 -0
- package/src/renderer/components/MultiSplitPane.tsx +251 -0
- package/src/renderer/components/PaneLayout.tsx +419 -0
- package/src/renderer/components/SettingsDialog.tsx +204 -0
- package/src/renderer/components/SplitPane.tsx +82 -0
- package/src/renderer/components/StatusBar.tsx +73 -0
- package/src/renderer/components/TerminalPanel.tsx +140 -0
- package/src/renderer/index.html +12 -0
- package/src/renderer/main.tsx +10 -0
- package/src/renderer/styles.css +722 -0
- package/tsconfig.json +8 -0
- package/tsconfig.main.json +15 -0
- package/tsconfig.preload.json +14 -0
- package/tsconfig.renderer.json +15 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
interface ProfileSummary {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
baseUrl: string
|
|
7
|
+
hasApiKey: boolean
|
|
8
|
+
dataspacePageId: string | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ProfileListResponse {
|
|
12
|
+
activeProfile: string
|
|
13
|
+
profiles: ProfileSummary[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface SettingsDialogProps {
|
|
17
|
+
open: boolean
|
|
18
|
+
onClose: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function SettingsDialog({ open, onClose }: SettingsDialogProps) {
|
|
22
|
+
const [profileList, setProfileList] = useState<ProfileListResponse | null>(null)
|
|
23
|
+
const [activeProfileId, setActiveProfileId] = useState('production')
|
|
24
|
+
const [editingId, setEditingId] = useState<string | null>(null)
|
|
25
|
+
|
|
26
|
+
// Edit form state
|
|
27
|
+
const [name, setName] = useState('')
|
|
28
|
+
const [apiKey, setApiKey] = useState('')
|
|
29
|
+
const [baseUrl, setBaseUrl] = useState('')
|
|
30
|
+
const [dataspacePageId, setDataspacePageId] = useState('')
|
|
31
|
+
const [saved, setSaved] = useState(false)
|
|
32
|
+
|
|
33
|
+
const loadProfiles = async () => {
|
|
34
|
+
const data = await window.worker.listProfiles() as ProfileListResponse
|
|
35
|
+
setProfileList(data)
|
|
36
|
+
setActiveProfileId(data.activeProfile)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (open) {
|
|
41
|
+
loadProfiles()
|
|
42
|
+
setEditingId(null)
|
|
43
|
+
setSaved(false)
|
|
44
|
+
}
|
|
45
|
+
}, [open])
|
|
46
|
+
|
|
47
|
+
const startEditing = (profile: ProfileSummary) => {
|
|
48
|
+
setEditingId(profile.id)
|
|
49
|
+
setName(profile.name)
|
|
50
|
+
setApiKey('')
|
|
51
|
+
setBaseUrl(profile.baseUrl)
|
|
52
|
+
setDataspacePageId(profile.dataspacePageId || '')
|
|
53
|
+
setSaved(false)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const startNewProfile = () => {
|
|
57
|
+
const id = `custom_${Date.now()}`
|
|
58
|
+
setEditingId(id)
|
|
59
|
+
setName('')
|
|
60
|
+
setApiKey('')
|
|
61
|
+
setBaseUrl('http://localhost:8000')
|
|
62
|
+
setDataspacePageId('')
|
|
63
|
+
setSaved(false)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const handleSave = async () => {
|
|
67
|
+
if (!editingId || !name.trim()) return
|
|
68
|
+
|
|
69
|
+
const data: { name: string; apiKey?: string; baseUrl: string; dataspacePageId: string } = {
|
|
70
|
+
name: name.trim(),
|
|
71
|
+
baseUrl: baseUrl.trim() || 'https://app.ctlsurf.com',
|
|
72
|
+
dataspacePageId: dataspacePageId.trim(),
|
|
73
|
+
}
|
|
74
|
+
// Only send apiKey if user typed something new
|
|
75
|
+
if (apiKey.trim()) {
|
|
76
|
+
data.apiKey = apiKey.trim()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await window.worker.saveProfile(editingId, data)
|
|
80
|
+
setSaved(true)
|
|
81
|
+
setTimeout(async () => {
|
|
82
|
+
setSaved(false)
|
|
83
|
+
setEditingId(null)
|
|
84
|
+
await loadProfiles()
|
|
85
|
+
}, 600)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const handleSwitch = async (profileId: string) => {
|
|
89
|
+
await window.worker.switchProfile(profileId)
|
|
90
|
+
setActiveProfileId(profileId)
|
|
91
|
+
await loadProfiles()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const handleDelete = async (profileId: string) => {
|
|
95
|
+
await window.worker.deleteProfile(profileId)
|
|
96
|
+
await loadProfiles()
|
|
97
|
+
setEditingId(null)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!open) return null
|
|
101
|
+
|
|
102
|
+
const profiles = profileList?.profiles || []
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className="settings-overlay" onClick={onClose}>
|
|
106
|
+
<div className="settings-dialog" onClick={e => e.stopPropagation()}>
|
|
107
|
+
<h3>Settings</h3>
|
|
108
|
+
|
|
109
|
+
{editingId ? (
|
|
110
|
+
// ─── Profile Edit Form ─────────────────
|
|
111
|
+
<div className="settings-section">
|
|
112
|
+
<h4>{profiles.find(p => p.id === editingId) ? 'Edit Profile' : 'New Profile'}</h4>
|
|
113
|
+
|
|
114
|
+
<label>
|
|
115
|
+
Profile Name
|
|
116
|
+
<input
|
|
117
|
+
type="text"
|
|
118
|
+
value={name}
|
|
119
|
+
onChange={e => setName(e.target.value)}
|
|
120
|
+
placeholder="e.g. Local Dev"
|
|
121
|
+
/>
|
|
122
|
+
</label>
|
|
123
|
+
|
|
124
|
+
<label>
|
|
125
|
+
Base URL
|
|
126
|
+
<input
|
|
127
|
+
type="text"
|
|
128
|
+
value={baseUrl}
|
|
129
|
+
onChange={e => setBaseUrl(e.target.value)}
|
|
130
|
+
placeholder="https://app.ctlsurf.com"
|
|
131
|
+
/>
|
|
132
|
+
<span className="settings-hint">
|
|
133
|
+
Use http://localhost:8000 for local development.
|
|
134
|
+
</span>
|
|
135
|
+
</label>
|
|
136
|
+
|
|
137
|
+
<label>
|
|
138
|
+
API Key
|
|
139
|
+
<input
|
|
140
|
+
type="password"
|
|
141
|
+
value={apiKey}
|
|
142
|
+
onChange={e => setApiKey(e.target.value)}
|
|
143
|
+
placeholder={profiles.find(p => p.id === editingId)?.hasApiKey ? '***configured*** (leave blank to keep)' : 'cpk_...'}
|
|
144
|
+
/>
|
|
145
|
+
</label>
|
|
146
|
+
|
|
147
|
+
<label>
|
|
148
|
+
Dataspace Page ID
|
|
149
|
+
<input
|
|
150
|
+
type="text"
|
|
151
|
+
value={dataspacePageId}
|
|
152
|
+
onChange={e => setDataspacePageId(e.target.value)}
|
|
153
|
+
placeholder="UUID (optional)"
|
|
154
|
+
/>
|
|
155
|
+
</label>
|
|
156
|
+
|
|
157
|
+
<div className="settings-actions">
|
|
158
|
+
<button className="btn-secondary" onClick={() => setEditingId(null)}>Back</button>
|
|
159
|
+
<button className="btn-primary" onClick={handleSave}>
|
|
160
|
+
{saved ? 'Saved!' : 'Save'}
|
|
161
|
+
</button>
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
) : (
|
|
165
|
+
// ─── Profile List ──────────────────────
|
|
166
|
+
<div className="settings-section">
|
|
167
|
+
<h4>Profiles</h4>
|
|
168
|
+
<p className="settings-hint">
|
|
169
|
+
Switch between production and custom environments.
|
|
170
|
+
</p>
|
|
171
|
+
|
|
172
|
+
<div className="profile-list">
|
|
173
|
+
{profiles.map(p => (
|
|
174
|
+
<div
|
|
175
|
+
key={p.id}
|
|
176
|
+
className={`profile-item ${p.id === activeProfileId ? 'active' : ''}`}
|
|
177
|
+
>
|
|
178
|
+
<div className="profile-info" onClick={() => handleSwitch(p.id)}>
|
|
179
|
+
<span className={`profile-dot ${p.id === activeProfileId ? 'active' : ''}`} />
|
|
180
|
+
<div>
|
|
181
|
+
<div className="profile-name">{p.name}</div>
|
|
182
|
+
<div className="profile-url">{p.baseUrl}</div>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
<div className="profile-actions">
|
|
186
|
+
<button className="btn-ghost" onClick={() => startEditing(p)}>Edit</button>
|
|
187
|
+
{p.id !== 'production' && (
|
|
188
|
+
<button className="btn-ghost btn-danger" onClick={() => handleDelete(p.id)}>×</button>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
))}
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div className="settings-actions">
|
|
196
|
+
<button className="btn-secondary" onClick={startNewProfile}>+ New Profile</button>
|
|
197
|
+
<button className="btn-secondary" onClick={onClose}>Close</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
)
|
|
204
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from 'react'
|
|
2
|
+
|
|
3
|
+
interface SplitPaneProps {
|
|
4
|
+
left: React.ReactNode
|
|
5
|
+
right: React.ReactNode
|
|
6
|
+
initialSplit?: number // 0-100 percentage for left panel
|
|
7
|
+
minLeft?: number // minimum % for left
|
|
8
|
+
minRight?: number // minimum % for right
|
|
9
|
+
onResize?: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function SplitPane({
|
|
13
|
+
left,
|
|
14
|
+
right,
|
|
15
|
+
initialSplit = 50,
|
|
16
|
+
minLeft = 20,
|
|
17
|
+
minRight = 20,
|
|
18
|
+
onResize
|
|
19
|
+
}: SplitPaneProps) {
|
|
20
|
+
const [split, setSplit] = useState(initialSplit)
|
|
21
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
22
|
+
const dragging = useRef(false)
|
|
23
|
+
|
|
24
|
+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
25
|
+
e.preventDefault()
|
|
26
|
+
dragging.current = true
|
|
27
|
+
document.body.style.cursor = 'col-resize'
|
|
28
|
+
document.body.style.userSelect = 'none'
|
|
29
|
+
}, [])
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const handleMouseMove = (e: MouseEvent) => {
|
|
33
|
+
if (!dragging.current || !containerRef.current) return
|
|
34
|
+
const rect = containerRef.current.getBoundingClientRect()
|
|
35
|
+
const pct = ((e.clientX - rect.left) / rect.width) * 100
|
|
36
|
+
const clamped = Math.max(minLeft, Math.min(100 - minRight, pct))
|
|
37
|
+
setSplit(clamped)
|
|
38
|
+
onResize?.()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const handleMouseUp = () => {
|
|
42
|
+
if (dragging.current) {
|
|
43
|
+
dragging.current = false
|
|
44
|
+
document.body.style.cursor = ''
|
|
45
|
+
document.body.style.userSelect = ''
|
|
46
|
+
onResize?.()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
window.addEventListener('mousemove', handleMouseMove)
|
|
51
|
+
window.addEventListener('mouseup', handleMouseUp)
|
|
52
|
+
return () => {
|
|
53
|
+
window.removeEventListener('mousemove', handleMouseMove)
|
|
54
|
+
window.removeEventListener('mouseup', handleMouseUp)
|
|
55
|
+
}
|
|
56
|
+
}, [minLeft, minRight, onResize])
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="split-pane" ref={containerRef}>
|
|
60
|
+
<div className="split-pane-left" style={{ width: `${split}%` }}>
|
|
61
|
+
{left}
|
|
62
|
+
</div>
|
|
63
|
+
<div
|
|
64
|
+
className="split-pane-divider"
|
|
65
|
+
onMouseDown={handleMouseDown}
|
|
66
|
+
onDoubleClick={() => { setSplit(50); onResize?.() }}
|
|
67
|
+
/>
|
|
68
|
+
<div className="split-pane-right" style={{ width: `${100 - split}%` }}>
|
|
69
|
+
{right}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Expose preset setter for keyboard shortcuts
|
|
76
|
+
export type SplitPreset = 'terminal' | 'balanced' | 'ctlsurf'
|
|
77
|
+
|
|
78
|
+
export const presetValues: Record<SplitPreset, number> = {
|
|
79
|
+
terminal: 75,
|
|
80
|
+
balanced: 50,
|
|
81
|
+
ctlsurf: 25
|
|
82
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
interface StatusBarProps {
|
|
4
|
+
wsStatus: string
|
|
5
|
+
cwd: string | null
|
|
6
|
+
onChangeCwd: () => void
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function StatusBar({ wsStatus, cwd, onChangeCwd }: StatusBarProps) {
|
|
10
|
+
const [creating, setCreating] = useState(false)
|
|
11
|
+
|
|
12
|
+
const wsLabel = {
|
|
13
|
+
disconnected: 'Disconnected',
|
|
14
|
+
connecting: 'Connecting…',
|
|
15
|
+
connected: 'Connected',
|
|
16
|
+
pending_approval: 'Pending Approval',
|
|
17
|
+
}[wsStatus] || wsStatus
|
|
18
|
+
|
|
19
|
+
const wsDotClass = {
|
|
20
|
+
disconnected: 'inactive',
|
|
21
|
+
connecting: 'idle',
|
|
22
|
+
connected: 'active',
|
|
23
|
+
pending_approval: 'pending',
|
|
24
|
+
no_project: 'idle',
|
|
25
|
+
}[wsStatus] || 'inactive'
|
|
26
|
+
|
|
27
|
+
const isNoProject = wsStatus === 'no_project'
|
|
28
|
+
|
|
29
|
+
const handleCreate = async () => {
|
|
30
|
+
setCreating(true)
|
|
31
|
+
try {
|
|
32
|
+
const result = await window.worker.createProject()
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
console.error('[statusbar] Failed to create project:', result.error)
|
|
35
|
+
}
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error('[statusbar] Create project error:', err)
|
|
38
|
+
} finally {
|
|
39
|
+
setCreating(false)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Shorten home directory for display
|
|
44
|
+
const home = '/Users/' + (cwd?.split('/')[2] || '')
|
|
45
|
+
const displayPath = cwd?.replace(home, '~') || ''
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="statusbar">
|
|
49
|
+
<div className="statusbar-section">
|
|
50
|
+
<span className="statusbar-path" onClick={onChangeCwd} title="Click to change directory">
|
|
51
|
+
{displayPath || 'No directory'}
|
|
52
|
+
</span>
|
|
53
|
+
</div>
|
|
54
|
+
<div className="statusbar-section">
|
|
55
|
+
<span className={`status-dot ${wsDotClass}`} />
|
|
56
|
+
{isNoProject ? (
|
|
57
|
+
<>
|
|
58
|
+
<span>Not linked to a ctlsurf project</span>
|
|
59
|
+
<button
|
|
60
|
+
className="statusbar-btn"
|
|
61
|
+
onClick={handleCreate}
|
|
62
|
+
disabled={creating}
|
|
63
|
+
>
|
|
64
|
+
{creating ? 'Creating…' : '+ Create Project'}
|
|
65
|
+
</button>
|
|
66
|
+
</>
|
|
67
|
+
) : (
|
|
68
|
+
<span>ctlsurf: {wsLabel}</span>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
import { Terminal } from '@xterm/xterm'
|
|
3
|
+
import { FitAddon } from '@xterm/addon-fit'
|
|
4
|
+
import { WebLinksAddon } from '@xterm/addon-web-links'
|
|
5
|
+
import '@xterm/xterm/css/xterm.css'
|
|
6
|
+
|
|
7
|
+
interface AgentConfig {
|
|
8
|
+
id: string
|
|
9
|
+
name: string
|
|
10
|
+
command: string
|
|
11
|
+
args: string[]
|
|
12
|
+
description: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface TerminalPanelProps {
|
|
16
|
+
agent: AgentConfig | null
|
|
17
|
+
onSpawn: (agent: AgentConfig) => Promise<void>
|
|
18
|
+
onExit: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function TerminalPanel({ agent, onSpawn, onExit }: TerminalPanelProps) {
|
|
22
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
23
|
+
const terminalRef = useRef<Terminal | null>(null)
|
|
24
|
+
const fitAddonRef = useRef<FitAddon | null>(null)
|
|
25
|
+
const currentAgentRef = useRef<string | null>(null)
|
|
26
|
+
|
|
27
|
+
// Initialize terminal once
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!containerRef.current || terminalRef.current) return
|
|
30
|
+
|
|
31
|
+
const terminal = new Terminal({
|
|
32
|
+
cursorBlink: true,
|
|
33
|
+
fontSize: 14,
|
|
34
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', monospace",
|
|
35
|
+
theme: {
|
|
36
|
+
background: '#1a1b26',
|
|
37
|
+
foreground: '#a9b1d6',
|
|
38
|
+
cursor: '#c0caf5',
|
|
39
|
+
selectionBackground: '#33467C',
|
|
40
|
+
black: '#15161e',
|
|
41
|
+
red: '#f7768e',
|
|
42
|
+
green: '#9ece6a',
|
|
43
|
+
yellow: '#e0af68',
|
|
44
|
+
blue: '#7aa2f7',
|
|
45
|
+
magenta: '#bb9af7',
|
|
46
|
+
cyan: '#7dcfff',
|
|
47
|
+
white: '#a9b1d6',
|
|
48
|
+
brightBlack: '#414868',
|
|
49
|
+
brightRed: '#f7768e',
|
|
50
|
+
brightGreen: '#9ece6a',
|
|
51
|
+
brightYellow: '#e0af68',
|
|
52
|
+
brightBlue: '#7aa2f7',
|
|
53
|
+
brightMagenta: '#bb9af7',
|
|
54
|
+
brightCyan: '#7dcfff',
|
|
55
|
+
brightWhite: '#c0caf5'
|
|
56
|
+
},
|
|
57
|
+
scrollback: 10000,
|
|
58
|
+
allowTransparency: false
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const fitAddon = new FitAddon()
|
|
62
|
+
terminal.loadAddon(fitAddon)
|
|
63
|
+
terminal.loadAddon(new WebLinksAddon())
|
|
64
|
+
|
|
65
|
+
terminal.open(containerRef.current)
|
|
66
|
+
fitAddon.fit()
|
|
67
|
+
|
|
68
|
+
terminalRef.current = terminal
|
|
69
|
+
fitAddonRef.current = fitAddon
|
|
70
|
+
|
|
71
|
+
// Send keystrokes to pty
|
|
72
|
+
terminal.onData((data) => {
|
|
73
|
+
window.worker.writePty(data)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Receive pty output
|
|
77
|
+
const unsubData = window.worker.onPtyData((data) => {
|
|
78
|
+
terminal.write(data)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Handle pty exit
|
|
82
|
+
const unsubExit = window.worker.onPtyExit((code) => {
|
|
83
|
+
terminal.writeln(`\r\n\x1b[33m[Process exited with code ${code}]\x1b[0m`)
|
|
84
|
+
onExit()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// Resize handling
|
|
88
|
+
let resizeTimeout: ReturnType<typeof setTimeout>
|
|
89
|
+
const handleResize = () => {
|
|
90
|
+
clearTimeout(resizeTimeout)
|
|
91
|
+
resizeTimeout = setTimeout(() => {
|
|
92
|
+
if (fitAddonRef.current && terminalRef.current) {
|
|
93
|
+
fitAddonRef.current.fit()
|
|
94
|
+
const { cols, rows } = terminalRef.current
|
|
95
|
+
window.worker.resizePty(cols, rows)
|
|
96
|
+
}
|
|
97
|
+
}, 150)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
window.addEventListener('resize', handleResize)
|
|
101
|
+
const observer = new ResizeObserver(handleResize)
|
|
102
|
+
observer.observe(containerRef.current)
|
|
103
|
+
|
|
104
|
+
return () => {
|
|
105
|
+
clearTimeout(resizeTimeout)
|
|
106
|
+
window.removeEventListener('resize', handleResize)
|
|
107
|
+
observer.disconnect()
|
|
108
|
+
unsubData()
|
|
109
|
+
unsubExit()
|
|
110
|
+
terminal.dispose()
|
|
111
|
+
terminalRef.current = null
|
|
112
|
+
fitAddonRef.current = null
|
|
113
|
+
}
|
|
114
|
+
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
115
|
+
|
|
116
|
+
// Spawn agent when it changes
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!agent || !terminalRef.current) return
|
|
119
|
+
if (currentAgentRef.current === agent.id) return
|
|
120
|
+
|
|
121
|
+
currentAgentRef.current = agent.id
|
|
122
|
+
|
|
123
|
+
// Clear terminal for new agent
|
|
124
|
+
terminalRef.current.clear()
|
|
125
|
+
|
|
126
|
+
onSpawn(agent).then(() => {
|
|
127
|
+
// Send initial resize after spawn
|
|
128
|
+
if (fitAddonRef.current && terminalRef.current) {
|
|
129
|
+
fitAddonRef.current.fit()
|
|
130
|
+
const { cols, rows } = terminalRef.current
|
|
131
|
+
window.worker.resizePty(cols, rows)
|
|
132
|
+
}
|
|
133
|
+
terminalRef.current?.focus()
|
|
134
|
+
})
|
|
135
|
+
}, [agent, onSpawn])
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<div className="terminal-container" ref={containerRef} />
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>ctlsurf-worker</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="./main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|