@phenx-inc/ctlsurf 0.1.16 → 0.1.18
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/main/index.js +278 -48
- package/out/renderer/assets/{cssMode-CihsrbZY.js → cssMode-Cxe23-tB.js} +3 -3
- package/out/renderer/assets/{freemarker2-DFrJ_l05.js → freemarker2-Be0nj7Oa.js} +1 -1
- package/out/renderer/assets/{handlebars-BZi7_LdH.js → handlebars-C0It7_Nu.js} +1 -1
- package/out/renderer/assets/{html-CdaPU_YJ.js → html-BW6LB-7J.js} +1 -1
- package/out/renderer/assets/{htmlMode-DZnUcBOX.js → htmlMode-D_V-1VlE.js} +3 -3
- package/out/renderer/assets/{index-CKhIh5ZQ.js → index-D568SpEt.js} +117 -145
- package/out/renderer/assets/{javascript-DvV4owMk.js → javascript-D_LoeNc7.js} +2 -2
- package/out/renderer/assets/{jsonMode-DX3yX_PF.js → jsonMode-K3WSinSE.js} +3 -3
- package/out/renderer/assets/{liquid-c7QWTywx.js → liquid-BqfOd6m8.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Dp-OZOZS.js → lspLanguageFeatures-Bjf28WU6.js} +1 -1
- package/out/renderer/assets/{mdx-Bi7NE1tt.js → mdx-BoESjI38.js} +1 -1
- package/out/renderer/assets/{python-BeMAHtzr.js → python-DlafOOgB.js} +1 -1
- package/out/renderer/assets/{razor-Cfc9e2QR.js → razor-CB6E9DBD.js} +1 -1
- package/out/renderer/assets/{tsMode-CKrhuOxD.js → tsMode-DYu1z_nn.js} +1 -1
- package/out/renderer/assets/{typescript-BEFZ19OD.js → typescript-CDjkh0d5.js} +1 -1
- package/out/renderer/assets/{xml-CVdJPpsd.js → xml-C-XlilKZ.js} +1 -1
- package/out/renderer/assets/{yaml-B_Px-4th.js → yaml-BTrtxLEo.js} +1 -1
- package/out/renderer/index.html +1 -1
- package/package.json +5 -1
- package/src/renderer/App.tsx +8 -25
- package/src/renderer/components/PaneLayout.tsx +6 -44
- package/src/renderer/components/TerminalPanel.tsx +100 -82
package/src/renderer/App.tsx
CHANGED
|
@@ -72,15 +72,6 @@ const DEFAULT_LAYOUT: LayoutNode = {
|
|
|
72
72
|
|
|
73
73
|
const ALL_PANE_IDS = ['editor', 'terminal', 'ctlsurf']
|
|
74
74
|
|
|
75
|
-
const offscreenStyle: React.CSSProperties = {
|
|
76
|
-
position: 'absolute',
|
|
77
|
-
width: '1px',
|
|
78
|
-
height: '1px',
|
|
79
|
-
overflow: 'hidden',
|
|
80
|
-
opacity: 0,
|
|
81
|
-
pointerEvents: 'none',
|
|
82
|
-
}
|
|
83
|
-
|
|
84
75
|
export default function App() {
|
|
85
76
|
const [agents, setAgents] = useState<AgentConfig[]>([])
|
|
86
77
|
const [selectedAgent, setSelectedAgent] = useState<AgentConfig | null>(null)
|
|
@@ -226,26 +217,19 @@ export default function App() {
|
|
|
226
217
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
227
218
|
}, [togglePane])
|
|
228
219
|
|
|
229
|
-
//
|
|
220
|
+
// Build pane contents (always rendered, layout controls visibility)
|
|
230
221
|
const panes: PaneContent[] = [
|
|
231
|
-
{ id: 'editor', label: 'Editor', content:
|
|
232
|
-
{
|
|
233
|
-
|
|
222
|
+
{ id: 'editor', label: 'Editor', content: <EditorPanel cwd={cwd} /> },
|
|
223
|
+
{
|
|
224
|
+
id: 'terminal',
|
|
225
|
+
label: 'Terminal',
|
|
226
|
+
content: <TerminalPanel agent={selectedAgent} onSpawn={handleSpawn} onExit={handleExit} />,
|
|
227
|
+
},
|
|
228
|
+
{ id: 'ctlsurf', label: 'ctlsurf', content: <CtlsurfPanel /> },
|
|
234
229
|
]
|
|
235
230
|
|
|
236
231
|
return (
|
|
237
232
|
<div className="app">
|
|
238
|
-
{/* Persistent component instances — never unmounted, positioned by portal targets */}
|
|
239
|
-
<div id="pane-host-editor" style={visiblePaneIds.includes('editor') ? undefined : offscreenStyle}>
|
|
240
|
-
<EditorPanel cwd={cwd} />
|
|
241
|
-
</div>
|
|
242
|
-
<div id="pane-host-terminal" style={visiblePaneIds.includes('terminal') ? undefined : offscreenStyle}>
|
|
243
|
-
<TerminalPanel agent={selectedAgent} onSpawn={handleSpawn} onExit={handleExit} />
|
|
244
|
-
</div>
|
|
245
|
-
<div id="pane-host-ctlsurf" style={visiblePaneIds.includes('ctlsurf') ? undefined : offscreenStyle}>
|
|
246
|
-
<CtlsurfPanel />
|
|
247
|
-
</div>
|
|
248
|
-
|
|
249
233
|
<div className="titlebar">
|
|
250
234
|
<span className="titlebar-title">ctlsurf-worker</span>
|
|
251
235
|
<div className="titlebar-controls">
|
|
@@ -288,7 +272,6 @@ export default function App() {
|
|
|
288
272
|
panes={panes.filter(p => visiblePaneIds.includes(p.id))}
|
|
289
273
|
onLayoutChange={setLayout}
|
|
290
274
|
onToggle={togglePane}
|
|
291
|
-
paneHostPrefix="pane-host-"
|
|
292
275
|
/>
|
|
293
276
|
)}
|
|
294
277
|
</div>
|
|
@@ -116,10 +116,9 @@ interface PaneLayoutProps {
|
|
|
116
116
|
panes: PaneContent[]
|
|
117
117
|
onLayoutChange: (layout: LayoutNode) => void
|
|
118
118
|
onToggle: (id: string) => void
|
|
119
|
-
paneHostPrefix?: string
|
|
120
119
|
}
|
|
121
120
|
|
|
122
|
-
export function PaneLayout({ layout, panes, onLayoutChange, onToggle
|
|
121
|
+
export function PaneLayout({ layout, panes, onLayoutChange, onToggle }: PaneLayoutProps) {
|
|
123
122
|
const [dragPaneId, setDragPaneId] = useState<string | null>(null)
|
|
124
123
|
|
|
125
124
|
const handleDrop = useCallback((targetPaneId: string, zone: DropZone) => {
|
|
@@ -151,7 +150,6 @@ export function PaneLayout({ layout, panes, onLayoutChange, onToggle, paneHostPr
|
|
|
151
150
|
onLayoutChange={onLayoutChange}
|
|
152
151
|
parentDirection={null}
|
|
153
152
|
path={[]}
|
|
154
|
-
paneHostPrefix={paneHostPrefix}
|
|
155
153
|
/>
|
|
156
154
|
</div>
|
|
157
155
|
</DragContext.Provider>
|
|
@@ -168,10 +166,9 @@ interface LayoutRendererProps {
|
|
|
168
166
|
onLayoutChange: (layout: LayoutNode) => void
|
|
169
167
|
parentDirection: SplitDirection | null
|
|
170
168
|
path: number[]
|
|
171
|
-
paneHostPrefix?: string
|
|
172
169
|
}
|
|
173
170
|
|
|
174
|
-
function LayoutRenderer({ node, paneMap, onDrop, onToggle, onLayoutChange, parentDirection, path
|
|
171
|
+
function LayoutRenderer({ node, paneMap, onDrop, onToggle, onLayoutChange, parentDirection, path }: LayoutRendererProps) {
|
|
175
172
|
if (node.type === 'leaf') {
|
|
176
173
|
const pane = paneMap.get(node.paneId)
|
|
177
174
|
if (!pane) return null
|
|
@@ -180,7 +177,6 @@ function LayoutRenderer({ node, paneMap, onDrop, onToggle, onLayoutChange, paren
|
|
|
180
177
|
pane={pane}
|
|
181
178
|
onDrop={onDrop}
|
|
182
179
|
onToggle={onToggle}
|
|
183
|
-
paneHostPrefix={paneHostPrefix}
|
|
184
180
|
/>
|
|
185
181
|
)
|
|
186
182
|
}
|
|
@@ -193,7 +189,6 @@ function LayoutRenderer({ node, paneMap, onDrop, onToggle, onLayoutChange, paren
|
|
|
193
189
|
onToggle={onToggle}
|
|
194
190
|
onLayoutChange={onLayoutChange}
|
|
195
191
|
path={path}
|
|
196
|
-
paneHostPrefix={paneHostPrefix}
|
|
197
192
|
/>
|
|
198
193
|
)
|
|
199
194
|
}
|
|
@@ -207,10 +202,9 @@ interface SplitContainerProps {
|
|
|
207
202
|
onToggle: (id: string) => void
|
|
208
203
|
onLayoutChange: (layout: LayoutNode) => void
|
|
209
204
|
path: number[]
|
|
210
|
-
paneHostPrefix?: string
|
|
211
205
|
}
|
|
212
206
|
|
|
213
|
-
function SplitContainer({ node, paneMap, onDrop, onToggle, onLayoutChange, path
|
|
207
|
+
function SplitContainer({ node, paneMap, onDrop, onToggle, onLayoutChange, path }: SplitContainerProps) {
|
|
214
208
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
215
209
|
const draggingDivider = useRef<number | null>(null)
|
|
216
210
|
const sizesRef = useRef(node.sizes)
|
|
@@ -312,7 +306,6 @@ function SplitContainer({ node, paneMap, onDrop, onToggle, onLayoutChange, path,
|
|
|
312
306
|
}}
|
|
313
307
|
parentDirection={node.direction}
|
|
314
308
|
path={[...path, i]}
|
|
315
|
-
paneHostPrefix={paneHostPrefix}
|
|
316
309
|
/>
|
|
317
310
|
</div>
|
|
318
311
|
{i < node.children.length - 1 && (
|
|
@@ -339,43 +332,12 @@ interface LeafPaneProps {
|
|
|
339
332
|
pane: PaneContent
|
|
340
333
|
onDrop: (targetPaneId: string, zone: DropZone) => void
|
|
341
334
|
onToggle: (id: string) => void
|
|
342
|
-
paneHostPrefix?: string
|
|
343
335
|
}
|
|
344
336
|
|
|
345
|
-
function LeafPane({ pane, onDrop, onToggle
|
|
337
|
+
function LeafPane({ pane, onDrop, onToggle }: LeafPaneProps) {
|
|
346
338
|
const { dragPaneId, setDragPaneId } = useContext(DragContext)
|
|
347
339
|
const [hoverZone, setHoverZone] = useState<DropZone | null>(null)
|
|
348
340
|
const paneRef = useRef<HTMLDivElement>(null)
|
|
349
|
-
const contentRef = useRef<HTMLDivElement>(null)
|
|
350
|
-
|
|
351
|
-
// Reparent the persistent host element into this leaf's content area
|
|
352
|
-
useEffect(() => {
|
|
353
|
-
if (!paneHostPrefix || !contentRef.current) return
|
|
354
|
-
const hostEl = document.getElementById(`${paneHostPrefix}${pane.id}`)
|
|
355
|
-
if (!hostEl) return
|
|
356
|
-
|
|
357
|
-
// Move host element into this container
|
|
358
|
-
hostEl.style.position = ''
|
|
359
|
-
hostEl.style.width = '100%'
|
|
360
|
-
hostEl.style.height = '100%'
|
|
361
|
-
hostEl.style.overflow = ''
|
|
362
|
-
hostEl.style.opacity = '1'
|
|
363
|
-
hostEl.style.pointerEvents = ''
|
|
364
|
-
contentRef.current.appendChild(hostEl)
|
|
365
|
-
|
|
366
|
-
return () => {
|
|
367
|
-
// When this leaf unmounts, move host back to offscreen
|
|
368
|
-
if (hostEl.parentElement === contentRef.current) {
|
|
369
|
-
document.body.appendChild(hostEl)
|
|
370
|
-
hostEl.style.position = 'absolute'
|
|
371
|
-
hostEl.style.width = '1px'
|
|
372
|
-
hostEl.style.height = '1px'
|
|
373
|
-
hostEl.style.overflow = 'hidden'
|
|
374
|
-
hostEl.style.opacity = '0'
|
|
375
|
-
hostEl.style.pointerEvents = 'none'
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}, [pane.id, paneHostPrefix])
|
|
379
341
|
|
|
380
342
|
const isDragging = dragPaneId === pane.id
|
|
381
343
|
const isDropTarget = dragPaneId !== null && dragPaneId !== pane.id
|
|
@@ -423,8 +385,8 @@ function LeafPane({ pane, onDrop, onToggle, paneHostPrefix }: LeafPaneProps) {
|
|
|
423
385
|
</div>
|
|
424
386
|
|
|
425
387
|
{/* Content */}
|
|
426
|
-
<div className="leaf-pane-content"
|
|
427
|
-
{
|
|
388
|
+
<div className="leaf-pane-content">
|
|
389
|
+
{pane.content}
|
|
428
390
|
</div>
|
|
429
391
|
|
|
430
392
|
{/* Drop zone overlay (only visible when another pane is being dragged) */}
|
|
@@ -18,82 +18,107 @@ interface TerminalPanelProps {
|
|
|
18
18
|
onExit: () => void
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// ─── Module-level singleton ──────────────────────────
|
|
22
|
+
// Survives React mount/unmount cycles so PTY connection is never lost.
|
|
23
|
+
|
|
24
|
+
let _terminal: Terminal | null = null
|
|
25
|
+
let _fitAddon: FitAddon | null = null
|
|
26
|
+
let _unsubData: (() => void) | null = null
|
|
27
|
+
let _unsubExit: (() => void) | null = null
|
|
28
|
+
let _currentAgentId: string | null = null
|
|
29
|
+
|
|
30
|
+
function getOrCreateTerminal(onExit: () => void): { terminal: Terminal; fitAddon: FitAddon } {
|
|
31
|
+
if (_terminal && _fitAddon) {
|
|
32
|
+
return { terminal: _terminal, fitAddon: _fitAddon }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const terminal = new Terminal({
|
|
36
|
+
cursorBlink: true,
|
|
37
|
+
fontSize: 14,
|
|
38
|
+
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', monospace",
|
|
39
|
+
theme: {
|
|
40
|
+
background: '#1a1b26',
|
|
41
|
+
foreground: '#a9b1d6',
|
|
42
|
+
cursor: '#c0caf5',
|
|
43
|
+
selectionBackground: '#33467C',
|
|
44
|
+
black: '#15161e',
|
|
45
|
+
red: '#f7768e',
|
|
46
|
+
green: '#9ece6a',
|
|
47
|
+
yellow: '#e0af68',
|
|
48
|
+
blue: '#7aa2f7',
|
|
49
|
+
magenta: '#bb9af7',
|
|
50
|
+
cyan: '#7dcfff',
|
|
51
|
+
white: '#a9b1d6',
|
|
52
|
+
brightBlack: '#414868',
|
|
53
|
+
brightRed: '#f7768e',
|
|
54
|
+
brightGreen: '#9ece6a',
|
|
55
|
+
brightYellow: '#e0af68',
|
|
56
|
+
brightBlue: '#7aa2f7',
|
|
57
|
+
brightMagenta: '#bb9af7',
|
|
58
|
+
brightCyan: '#7dcfff',
|
|
59
|
+
brightWhite: '#c0caf5'
|
|
60
|
+
},
|
|
61
|
+
scrollback: 10000,
|
|
62
|
+
scrollOnUserInput: true,
|
|
63
|
+
allowTransparency: false
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const fitAddon = new FitAddon()
|
|
67
|
+
terminal.loadAddon(fitAddon)
|
|
68
|
+
terminal.loadAddon(new WebLinksAddon())
|
|
69
|
+
|
|
70
|
+
// Send keystrokes to pty
|
|
71
|
+
terminal.onData((data) => {
|
|
72
|
+
window.worker.writePty(data)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Receive pty output — subscribed once, never unsubscribed
|
|
76
|
+
_unsubData = window.worker.onPtyData((data) => {
|
|
77
|
+
terminal.write(data)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Handle pty exit
|
|
81
|
+
_unsubExit = window.worker.onPtyExit((code) => {
|
|
82
|
+
terminal.writeln(`\r\n\x1b[33m[Process exited with code ${code}]\x1b[0m`)
|
|
83
|
+
onExit()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
_terminal = terminal
|
|
87
|
+
_fitAddon = fitAddon
|
|
88
|
+
return { terminal, fitAddon }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── Component ───────────────────────────────────────
|
|
92
|
+
|
|
21
93
|
export function TerminalPanel({ agent, onSpawn, onExit }: TerminalPanelProps) {
|
|
22
94
|
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
95
|
|
|
27
|
-
//
|
|
96
|
+
// Attach terminal to DOM container (or reattach on remount)
|
|
28
97
|
useEffect(() => {
|
|
29
|
-
if (!containerRef.current
|
|
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
|
-
scrollOnUserInput: true,
|
|
59
|
-
allowTransparency: false
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
const fitAddon = new FitAddon()
|
|
63
|
-
terminal.loadAddon(fitAddon)
|
|
64
|
-
terminal.loadAddon(new WebLinksAddon())
|
|
65
|
-
|
|
66
|
-
terminal.open(containerRef.current)
|
|
67
|
-
fitAddon.fit()
|
|
98
|
+
if (!containerRef.current) return
|
|
68
99
|
|
|
69
|
-
|
|
70
|
-
fitAddonRef.current = fitAddon
|
|
100
|
+
const { terminal, fitAddon } = getOrCreateTerminal(onExit)
|
|
71
101
|
|
|
72
|
-
//
|
|
73
|
-
terminal.
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
terminal.write(data)
|
|
80
|
-
})
|
|
102
|
+
// If terminal is already open in another container, move it
|
|
103
|
+
const existingParent = terminal.element?.parentElement
|
|
104
|
+
if (existingParent && existingParent !== containerRef.current) {
|
|
105
|
+
containerRef.current.appendChild(terminal.element!)
|
|
106
|
+
} else if (!terminal.element) {
|
|
107
|
+
terminal.open(containerRef.current)
|
|
108
|
+
}
|
|
81
109
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
terminal.writeln(`\r\n\x1b[33m[Process exited with code ${code}]\x1b[0m`)
|
|
85
|
-
onExit()
|
|
86
|
-
})
|
|
110
|
+
fitAddon.fit()
|
|
111
|
+
terminal.scrollToBottom()
|
|
87
112
|
|
|
88
|
-
// Resize handling
|
|
113
|
+
// Resize handling
|
|
89
114
|
let resizeTimeout: ReturnType<typeof setTimeout>
|
|
90
115
|
const handleResize = () => {
|
|
91
116
|
clearTimeout(resizeTimeout)
|
|
92
117
|
resizeTimeout = setTimeout(() => {
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const { cols, rows } =
|
|
118
|
+
if (_fitAddon && _terminal) {
|
|
119
|
+
_fitAddon.fit()
|
|
120
|
+
_terminal.scrollToBottom()
|
|
121
|
+
const { cols, rows } = _terminal
|
|
97
122
|
window.worker.resizePty(cols, rows)
|
|
98
123
|
}
|
|
99
124
|
}, 150)
|
|
@@ -107,33 +132,26 @@ export function TerminalPanel({ agent, onSpawn, onExit }: TerminalPanelProps) {
|
|
|
107
132
|
clearTimeout(resizeTimeout)
|
|
108
133
|
window.removeEventListener('resize', handleResize)
|
|
109
134
|
observer.disconnect()
|
|
110
|
-
|
|
111
|
-
unsubExit()
|
|
112
|
-
terminal.dispose()
|
|
113
|
-
terminalRef.current = null
|
|
114
|
-
fitAddonRef.current = null
|
|
135
|
+
// Do NOT dispose terminal or unsub PTY listeners
|
|
115
136
|
}
|
|
116
|
-
}, [])
|
|
137
|
+
}, [onExit])
|
|
117
138
|
|
|
118
139
|
// Spawn agent when it changes
|
|
119
140
|
useEffect(() => {
|
|
120
|
-
if (!agent || !
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
currentAgentRef.current = agent.id
|
|
141
|
+
if (!agent || !_terminal) return
|
|
142
|
+
if (_currentAgentId === agent.id) return
|
|
124
143
|
|
|
125
|
-
|
|
126
|
-
|
|
144
|
+
_currentAgentId = agent.id
|
|
145
|
+
_terminal.clear()
|
|
127
146
|
|
|
128
147
|
onSpawn(agent).then(() => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const { cols, rows } = terminalRef.current
|
|
148
|
+
if (_fitAddon && _terminal) {
|
|
149
|
+
_fitAddon.fit()
|
|
150
|
+
_terminal.scrollToBottom()
|
|
151
|
+
const { cols, rows } = _terminal
|
|
134
152
|
window.worker.resizePty(cols, rows)
|
|
135
153
|
}
|
|
136
|
-
|
|
154
|
+
_terminal?.focus()
|
|
137
155
|
})
|
|
138
156
|
}, [agent, onSpawn])
|
|
139
157
|
|