@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.
Files changed (133) hide show
  1. package/bin/ctlsurf-worker.js +173 -0
  2. package/electron-vite.config.ts +34 -0
  3. package/out/headless/index.mjs +1364 -0
  4. package/out/headless/index.mjs.map +7 -0
  5. package/out/main/index.js +1131 -0
  6. package/out/preload/index.js +67 -0
  7. package/out/renderer/assets/abap-D5KwWAsZ.js +1399 -0
  8. package/out/renderer/assets/apex-DVGUZ64i.js +331 -0
  9. package/out/renderer/assets/azcli-BEAhqcuE.js +69 -0
  10. package/out/renderer/assets/bat-Bqkp9Cfu.js +101 -0
  11. package/out/renderer/assets/bicep-DIlfshcM.js +110 -0
  12. package/out/renderer/assets/cameligo-CLaaYNMV.js +175 -0
  13. package/out/renderer/assets/clojure-fcgFaMHx.js +762 -0
  14. package/out/renderer/assets/codicon-ngg6Pgfi.ttf +0 -0
  15. package/out/renderer/assets/coffee-CzJ5oEdj.js +233 -0
  16. package/out/renderer/assets/cpp-CcN6f0ik.js +390 -0
  17. package/out/renderer/assets/csharp-BJeIuvde.js +327 -0
  18. package/out/renderer/assets/csp-D_3BK2Wp.js +54 -0
  19. package/out/renderer/assets/css-i3rI3_64.js +186 -0
  20. package/out/renderer/assets/css.worker-umuuUiIb.js +53567 -0
  21. package/out/renderer/assets/cssMode-DL0XItGB.js +208 -0
  22. package/out/renderer/assets/cypher-D0--_GAN.js +264 -0
  23. package/out/renderer/assets/dart-vLMHv35g.js +282 -0
  24. package/out/renderer/assets/dockerfile--oxj0cAH.js +131 -0
  25. package/out/renderer/assets/ecl-CeuUgzaZ.js +457 -0
  26. package/out/renderer/assets/editor.worker-CNgWLVu7.js +13695 -0
  27. package/out/renderer/assets/elixir-eLfY1jWH.js +570 -0
  28. package/out/renderer/assets/flow9-ZSTChSMd.js +143 -0
  29. package/out/renderer/assets/freemarker2-CrOEuDcF.js +995 -0
  30. package/out/renderer/assets/fsharp-D2uoxuLH.js +218 -0
  31. package/out/renderer/assets/go-brnMpFrj.js +219 -0
  32. package/out/renderer/assets/graphql-BeiGgjIU.js +152 -0
  33. package/out/renderer/assets/handlebars-D4QYaBof.js +414 -0
  34. package/out/renderer/assets/hcl-CrX1Es2W.js +184 -0
  35. package/out/renderer/assets/html-B2Dqk2ai.js +303 -0
  36. package/out/renderer/assets/html.worker-BT47iy49.js +29777 -0
  37. package/out/renderer/assets/htmlMode-CdZ0Prhd.js +224 -0
  38. package/out/renderer/assets/index-CJ6RsQWP.css +8108 -0
  39. package/out/renderer/assets/index-pZmE1QXB.js +211777 -0
  40. package/out/renderer/assets/ini-BcQysCTb.js +72 -0
  41. package/out/renderer/assets/java-Dt3iMn2o.js +233 -0
  42. package/out/renderer/assets/javascript-CK8zNQXj.js +72 -0
  43. package/out/renderer/assets/json.worker-D4JVmXIe.js +21424 -0
  44. package/out/renderer/assets/jsonMode-Cewaellc.js +931 -0
  45. package/out/renderer/assets/julia-Cm3ItYL_.js +512 -0
  46. package/out/renderer/assets/kotlin-Ddo1SjA5.js +253 -0
  47. package/out/renderer/assets/less-B7Qaxw-O.js +162 -0
  48. package/out/renderer/assets/lexon-C1U0m2n9.js +158 -0
  49. package/out/renderer/assets/liquid-Bd3GPNs2.js +235 -0
  50. package/out/renderer/assets/lspLanguageFeatures-DSDH7BnA.js +1841 -0
  51. package/out/renderer/assets/lua-hNsuGJkO.js +163 -0
  52. package/out/renderer/assets/m3-6ko6q9-_.js +211 -0
  53. package/out/renderer/assets/markdown-B0YTnTxW.js +230 -0
  54. package/out/renderer/assets/mdx-CCPVCrXC.js +159 -0
  55. package/out/renderer/assets/mips-CJm71dS3.js +199 -0
  56. package/out/renderer/assets/msdax-BBeIktCY.js +376 -0
  57. package/out/renderer/assets/mysql-BWiizXSn.js +879 -0
  58. package/out/renderer/assets/objective-c-B1L1C5EC.js +184 -0
  59. package/out/renderer/assets/pascal-DMQyD4Xk.js +252 -0
  60. package/out/renderer/assets/pascaligo-VA_LQ1oU.js +165 -0
  61. package/out/renderer/assets/perl-DC0Z0tlO.js +627 -0
  62. package/out/renderer/assets/pgsql-DaSGFTLp.js +852 -0
  63. package/out/renderer/assets/php-Bkx1qpkQ.js +501 -0
  64. package/out/renderer/assets/pla-DEV89yYj.js +138 -0
  65. package/out/renderer/assets/postiats-CVVurEnu.js +908 -0
  66. package/out/renderer/assets/powerquery-BQ_t1ZiQ.js +891 -0
  67. package/out/renderer/assets/powershell-BXiKvz7Z.js +240 -0
  68. package/out/renderer/assets/protobuf-CndvAUGu.js +421 -0
  69. package/out/renderer/assets/pug-BxCXwerb.js +403 -0
  70. package/out/renderer/assets/python-34jOtlcC.js +295 -0
  71. package/out/renderer/assets/qsharp-BWK6YLKm.js +302 -0
  72. package/out/renderer/assets/r-CtqYUQ6l.js +244 -0
  73. package/out/renderer/assets/razor-DXRw694z.js +545 -0
  74. package/out/renderer/assets/redis-O7gSt3oh.js +303 -0
  75. package/out/renderer/assets/redshift-CvYMMYZY.js +810 -0
  76. package/out/renderer/assets/restructuredtext-B-KQCVu_.js +175 -0
  77. package/out/renderer/assets/ruby-DCd4DmAr.js +512 -0
  78. package/out/renderer/assets/rust-B1c0VCeq.js +344 -0
  79. package/out/renderer/assets/sb-Chfc_wZF.js +116 -0
  80. package/out/renderer/assets/scala-DbVzH-3O.js +371 -0
  81. package/out/renderer/assets/scheme-D7PxodDG.js +109 -0
  82. package/out/renderer/assets/scss-B42qMyAu.js +261 -0
  83. package/out/renderer/assets/shell-vZEubQ82.js +222 -0
  84. package/out/renderer/assets/solidity-yHOxYChb.js +1368 -0
  85. package/out/renderer/assets/sophia-D7pU0Y1d.js +200 -0
  86. package/out/renderer/assets/sparql-DxuVdnRl.js +202 -0
  87. package/out/renderer/assets/sql-BAGepFCR.js +854 -0
  88. package/out/renderer/assets/st-C-b0Dh53.js +417 -0
  89. package/out/renderer/assets/swift-BmOZGynf.js +313 -0
  90. package/out/renderer/assets/systemverilog-BOC0OOdC.js +577 -0
  91. package/out/renderer/assets/tcl-Bb4GCwBr.js +233 -0
  92. package/out/renderer/assets/ts.worker-C7hW3aY-.js +225330 -0
  93. package/out/renderer/assets/tsMode-CmND5_wB.js +1265 -0
  94. package/out/renderer/assets/twig-DvgEGWAV.js +393 -0
  95. package/out/renderer/assets/typescript-BNNI0Euv.js +337 -0
  96. package/out/renderer/assets/typespec-R77Ln7Jb.js +128 -0
  97. package/out/renderer/assets/vb-Bm6ESA0Q.js +373 -0
  98. package/out/renderer/assets/wgsl-_KPae5vw.js +454 -0
  99. package/out/renderer/assets/xml-CgdndrNB.js +89 -0
  100. package/out/renderer/assets/yaml-DNWPIf1s.js +200 -0
  101. package/out/renderer/index.html +13 -0
  102. package/package.json +67 -0
  103. package/resources/icon.icns +0 -0
  104. package/resources/icon.ico +0 -0
  105. package/resources/icon.png +0 -0
  106. package/src/main/agents.ts +46 -0
  107. package/src/main/bridge.ts +180 -0
  108. package/src/main/ctlsurfApi.ts +142 -0
  109. package/src/main/detectMode.ts +17 -0
  110. package/src/main/headless.ts +182 -0
  111. package/src/main/index.ts +300 -0
  112. package/src/main/orchestrator.ts +404 -0
  113. package/src/main/pty.ts +65 -0
  114. package/src/main/settingsDir.ts +17 -0
  115. package/src/main/tui.ts +366 -0
  116. package/src/main/workerWs.ts +312 -0
  117. package/src/preload/index.ts +114 -0
  118. package/src/renderer/App.tsx +275 -0
  119. package/src/renderer/components/CtlsurfPanel.tsx +49 -0
  120. package/src/renderer/components/EditorPanel.tsx +232 -0
  121. package/src/renderer/components/MultiSplitPane.tsx +251 -0
  122. package/src/renderer/components/PaneLayout.tsx +419 -0
  123. package/src/renderer/components/SettingsDialog.tsx +204 -0
  124. package/src/renderer/components/SplitPane.tsx +82 -0
  125. package/src/renderer/components/StatusBar.tsx +73 -0
  126. package/src/renderer/components/TerminalPanel.tsx +140 -0
  127. package/src/renderer/index.html +12 -0
  128. package/src/renderer/main.tsx +10 -0
  129. package/src/renderer/styles.css +722 -0
  130. package/tsconfig.json +8 -0
  131. package/tsconfig.main.json +15 -0
  132. package/tsconfig.preload.json +14 -0
  133. 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>
@@ -0,0 +1,10 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App'
4
+ import './styles.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ )