@phenx-inc/ctlsurf 0.5.0 → 0.5.2
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 +36 -18
- package/out/headless/index.mjs.map +2 -2
- package/out/main/index.js +35 -6
- package/out/preload/index.js +3 -0
- package/out/renderer/assets/{cssMode-DkmdBgO7.js → cssMode-D9-xaWSI.js} +3 -3
- package/out/renderer/assets/{freemarker2-CI-gkP-3.js → freemarker2-CoRAVxnv.js} +1 -1
- package/out/renderer/assets/{handlebars-D5tEqanR.js → handlebars-B0p9Wgkw.js} +1 -1
- package/out/renderer/assets/{html-fH93EYfn.js → html-D_XFJJtO.js} +1 -1
- package/out/renderer/assets/{htmlMode-CRicxcwK.js → htmlMode-naWw6PWr.js} +3 -3
- package/out/renderer/assets/{index-BOOvUI7u.js → index-DBt_rov1.js} +64 -26
- package/out/renderer/assets/{javascript-D1Baz4fV.js → javascript-DDLsFUr-.js} +2 -2
- package/out/renderer/assets/{jsonMode-Bquqf3QN.js → jsonMode-Ixhcm5I6.js} +3 -3
- package/out/renderer/assets/{liquid-ByOcPjBF.js → liquid-BHgSYEHk.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-BxPLl0yy.js → lspLanguageFeatures-ClbEdD0U.js} +1 -1
- package/out/renderer/assets/{mdx-yuNgx0rM.js → mdx-DMngMjHR.js} +1 -1
- package/out/renderer/assets/{python-2OakgLlA.js → python-D_czoeY2.js} +1 -1
- package/out/renderer/assets/{razor-DnIVMSwa.js → razor-CLMDGvL7.js} +1 -1
- package/out/renderer/assets/{tsMode-CRIrHuii.js → tsMode-EIuSGG42.js} +1 -1
- package/out/renderer/assets/{typescript-DJ3C8Yly.js → typescript-DQkV4kKA.js} +1 -1
- package/out/renderer/assets/{xml-CalvD5_C.js → xml-DJ0OOQTu.js} +1 -1
- package/out/renderer/assets/{yaml-Cgs8pdVp.js → yaml-DxX26XLN.js} +1 -1
- package/out/renderer/index.html +1 -1
- package/package.json +1 -1
- package/src/main/index.ts +42 -2
- package/src/main/tui.ts +39 -17
- package/src/preload/index.ts +4 -0
- package/src/renderer/App.tsx +21 -1
- package/src/renderer/components/CtlsurfPanel.tsx +19 -3
- package/src/renderer/components/TerminalPanel.tsx +26 -1
package/src/main/tui.ts
CHANGED
|
@@ -140,6 +140,10 @@ export class Tui {
|
|
|
140
140
|
let selected = 0
|
|
141
141
|
let trackTime = options.initialTrackTime
|
|
142
142
|
let logChat = options.initialLogChat
|
|
143
|
+
// Navigable items: agent rows, then Track time and Log chat checkbox rows
|
|
144
|
+
const trackIdx = agents.length
|
|
145
|
+
const logIdx = agents.length + 1
|
|
146
|
+
const itemCount = agents.length + 2
|
|
143
147
|
const modalWidth = 44
|
|
144
148
|
// +4 for borders/title/sep, +3 for separator + track-time row + log-chat row
|
|
145
149
|
const modalHeight = agents.length + 4 + 3
|
|
@@ -190,26 +194,27 @@ export class Tui {
|
|
|
190
194
|
const sepRow = startRow + 3 + agents.length
|
|
191
195
|
this.write(`${CSI}${sepRow};${startCol}H${BG_MODAL}${FG_DIM}${innerSep}${RESET}`)
|
|
192
196
|
|
|
197
|
+
const drawCheckboxRow = (row: number, label: string, checked: boolean, isSelected: boolean) => {
|
|
198
|
+
const bg = isSelected ? BG_SELECTED : BG_MODAL
|
|
199
|
+
const pointer = isSelected ? `${FG_ACCENT}▸ ` : ' '
|
|
200
|
+
const checkbox = checked ? `${FG_GREEN}[✓]${RESET}${bg}` : `${FG_DIM}[ ]${RESET}${bg}`
|
|
201
|
+
const labelFg = checked || isSelected ? FG_WHITE : FG_DIM
|
|
202
|
+
const content = `${pointer}${checkbox} ${labelFg}${label}${RESET}${bg}`
|
|
203
|
+
const contentLen = 2 + 3 + 1 + label.length
|
|
204
|
+
const pad = ' '.repeat(Math.max(0, modalWidth - 2 - contentLen))
|
|
205
|
+
this.write(`${CSI}${row};${startCol}H${bg}${FG_DIM}│${RESET}${bg}${content}${pad}${RESET}${BG_MODAL}${FG_DIM}│${RESET}`)
|
|
206
|
+
}
|
|
207
|
+
|
|
193
208
|
const trackRow = sepRow + 1
|
|
194
|
-
|
|
195
|
-
const trackLabelFg = trackTime ? FG_WHITE : FG_DIM
|
|
196
|
-
const trackContent = ` ${checkbox} ${trackLabelFg}Track time${RESET}${BG_MODAL}`
|
|
197
|
-
const trackContentLen = 2 + 3 + 1 + 'Track time'.length
|
|
198
|
-
const trackPad = ' '.repeat(Math.max(0, modalWidth - 2 - trackContentLen))
|
|
199
|
-
this.write(`${CSI}${trackRow};${startCol}H${BG_MODAL}${FG_DIM}│${RESET}${BG_MODAL}${trackContent}${trackPad}${FG_DIM}│${RESET}`)
|
|
209
|
+
drawCheckboxRow(trackRow, 'Track time', trackTime, selected === trackIdx)
|
|
200
210
|
|
|
201
211
|
const logRow = trackRow + 1
|
|
202
|
-
|
|
203
|
-
const logLabelFg = logChat ? FG_WHITE : FG_DIM
|
|
204
|
-
const logContent = ` ${logCheckbox} ${logLabelFg}Log chat${RESET}${BG_MODAL}`
|
|
205
|
-
const logContentLen = 2 + 3 + 1 + 'Log chat'.length
|
|
206
|
-
const logPad = ' '.repeat(Math.max(0, modalWidth - 2 - logContentLen))
|
|
207
|
-
this.write(`${CSI}${logRow};${startCol}H${BG_MODAL}${FG_DIM}\u2502${RESET}${BG_MODAL}${logContent}${logPad}${FG_DIM}\u2502${RESET}`)
|
|
212
|
+
drawCheckboxRow(logRow, 'Log chat', logChat, selected === logIdx)
|
|
208
213
|
|
|
209
214
|
const botRow = logRow + 1
|
|
210
215
|
this.write(`${CSI}${botRow};${startCol}H${BG_MODAL}${FG_DIM}${botBorder}${RESET}`)
|
|
211
216
|
|
|
212
|
-
const hint = '\u2191\u2193 nav \u00B7 Enter
|
|
217
|
+
const hint = '\u2191\u2193 nav \u00B7 Enter select \u00B7 Space toggle \u00B7 q quit'
|
|
213
218
|
const hintCol = Math.max(1, Math.floor((this.cols - hint.length) / 2))
|
|
214
219
|
this.write(`${CSI}${botRow + 2};${hintCol}H${FG_DIM}${hint}${RESET}`)
|
|
215
220
|
}
|
|
@@ -224,11 +229,23 @@ export class Tui {
|
|
|
224
229
|
const onKey = (data: Buffer) => {
|
|
225
230
|
const key = data.toString()
|
|
226
231
|
|
|
232
|
+
const toggleSelected = (): boolean => {
|
|
233
|
+
if (selected === trackIdx) {
|
|
234
|
+
trackTime = !trackTime
|
|
235
|
+
} else if (selected === logIdx) {
|
|
236
|
+
logChat = !logChat
|
|
237
|
+
} else {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
drawModal()
|
|
241
|
+
return true
|
|
242
|
+
}
|
|
243
|
+
|
|
227
244
|
if (key === '\x1b[A' || key === 'k') {
|
|
228
|
-
selected = (selected - 1 +
|
|
245
|
+
selected = (selected - 1 + itemCount) % itemCount
|
|
229
246
|
drawModal()
|
|
230
247
|
} else if (key === '\x1b[B' || key === 'j') {
|
|
231
|
-
selected = (selected + 1) %
|
|
248
|
+
selected = (selected + 1) % itemCount
|
|
232
249
|
drawModal()
|
|
233
250
|
} else if (key === 't' || key === 'T') {
|
|
234
251
|
trackTime = !trackTime
|
|
@@ -236,9 +253,14 @@ export class Tui {
|
|
|
236
253
|
} else if (key === 'l' || key === 'L') {
|
|
237
254
|
logChat = !logChat
|
|
238
255
|
drawModal()
|
|
256
|
+
} else if (key === ' ') {
|
|
257
|
+
toggleSelected()
|
|
239
258
|
} else if (key === '\r' || key === '\n') {
|
|
240
|
-
|
|
241
|
-
|
|
259
|
+
// Enter on a checkbox row toggles it; on an agent row it confirms
|
|
260
|
+
if (!toggleSelected()) {
|
|
261
|
+
cleanup()
|
|
262
|
+
resolve({ agentIdx: selected, trackTime, logChat })
|
|
263
|
+
}
|
|
242
264
|
} else if (key === 'q' || key === '\x1b' || key === '\x03') {
|
|
243
265
|
cleanup()
|
|
244
266
|
this.write(`${CSI}?25h`)
|
package/src/preload/index.ts
CHANGED
|
@@ -129,6 +129,10 @@ const api = {
|
|
|
129
129
|
pageUrl?: string;
|
|
130
130
|
authenticated: boolean;
|
|
131
131
|
}> => ipcRenderer.invoke('worker:getWebviewInfo'),
|
|
132
|
+
// Resolve a terminal-printed URL to an authenticated ctlsurf embed URL, or
|
|
133
|
+
// null if it isn't a ctlsurf link (caller opens those externally).
|
|
134
|
+
getEmbedUrl: (targetUrl: string): Promise<string | null> =>
|
|
135
|
+
ipcRenderer.invoke('worker:getEmbedUrl', targetUrl),
|
|
132
136
|
|
|
133
137
|
onWorkerStatus: (callback: (status: string) => void) => {
|
|
134
138
|
const listener = (_event: Electron.IpcRendererEvent, status: string) => callback(status)
|
package/src/renderer/App.tsx
CHANGED
|
@@ -59,6 +59,7 @@ declare global {
|
|
|
59
59
|
getWebviewInfo: () => Promise<{
|
|
60
60
|
frontendUrl: string; pageUrl?: string; authenticated: boolean;
|
|
61
61
|
}>
|
|
62
|
+
getEmbedUrl: (targetUrl: string) => Promise<string | null>
|
|
62
63
|
getWorkerStatus: () => Promise<string>
|
|
63
64
|
getWorkerId: () => Promise<string | null>
|
|
64
65
|
onWorkerStatus: (callback: (status: string) => void) => () => void
|
|
@@ -117,6 +118,11 @@ export default function App() {
|
|
|
117
118
|
const [cwd, setCwd] = useState<string | null>(null)
|
|
118
119
|
const [projectName, setProjectName] = useState<string | null>(null)
|
|
119
120
|
const [updateInfo, setUpdateInfo] = useState<UpdateInfo | null>(null)
|
|
121
|
+
// A ctlsurf link clicked in the terminal: the authenticated URL to load in
|
|
122
|
+
// the ctlsurf panel. `n` bumps on every request so re-clicking the same URL
|
|
123
|
+
// still triggers a navigation.
|
|
124
|
+
const [ctlsurfNav, setCtlsurfNav] = useState<{ url: string; n: number } | null>(null)
|
|
125
|
+
const ctlsurfNavCounter = useRef(0)
|
|
120
126
|
|
|
121
127
|
// Multi-tab state
|
|
122
128
|
const [tabs, setTabs] = useState<TabInfo[]>(() => {
|
|
@@ -349,6 +355,20 @@ export default function App() {
|
|
|
349
355
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
350
356
|
}, [togglePane])
|
|
351
357
|
|
|
358
|
+
// A ctlsurf link was clicked in the terminal — reveal the ctlsurf pane if
|
|
359
|
+
// hidden and hand the authenticated URL to the panel to load.
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
const onOpen = (e: Event) => {
|
|
362
|
+
const url = (e as CustomEvent).detail?.url as string | undefined
|
|
363
|
+
if (!url) return
|
|
364
|
+
if (!findPaneIds(layout).includes('ctlsurf')) togglePane('ctlsurf')
|
|
365
|
+
ctlsurfNavCounter.current += 1
|
|
366
|
+
setCtlsurfNav({ url, n: ctlsurfNavCounter.current })
|
|
367
|
+
}
|
|
368
|
+
window.addEventListener('ctlsurf-open-url', onOpen)
|
|
369
|
+
return () => window.removeEventListener('ctlsurf-open-url', onOpen)
|
|
370
|
+
}, [layout, togglePane])
|
|
371
|
+
|
|
352
372
|
// Build pane contents (always rendered, layout controls visibility)
|
|
353
373
|
const panes: PaneContent[] = [
|
|
354
374
|
{ id: 'editor', label: 'Editor', content: <EditorPanel cwd={cwd} /> },
|
|
@@ -409,7 +429,7 @@ export default function App() {
|
|
|
409
429
|
</div>
|
|
410
430
|
),
|
|
411
431
|
},
|
|
412
|
-
{ id: 'ctlsurf', label: 'ctlsurf', content: <CtlsurfPanel /> },
|
|
432
|
+
{ id: 'ctlsurf', label: 'ctlsurf', content: <CtlsurfPanel navigate={ctlsurfNav} /> },
|
|
413
433
|
]
|
|
414
434
|
|
|
415
435
|
return (
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { useRef, useEffect, useState } from 'react'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
interface CtlsurfPanelProps {
|
|
4
|
+
// An authenticated ctlsurf URL to load (e.g. from a terminal link click).
|
|
5
|
+
// `n` bumps per request so repeat navigations to the same URL still apply.
|
|
6
|
+
navigate?: { url: string; n: number } | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function CtlsurfPanel({ navigate }: CtlsurfPanelProps) {
|
|
4
10
|
const webviewRef = useRef<HTMLWebViewElement>(null)
|
|
5
11
|
const [url, setUrl] = useState<string | null>(null)
|
|
6
|
-
const [key, setKey] = useState(0) // force remount on cwd change
|
|
12
|
+
const [key, setKey] = useState(0) // force remount on cwd change / navigation
|
|
7
13
|
|
|
8
14
|
const loadUrl = async () => {
|
|
9
15
|
try {
|
|
@@ -15,7 +21,17 @@ export function CtlsurfPanel() {
|
|
|
15
21
|
}
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
// Default-load the project page on mount, unless a navigation is already
|
|
25
|
+
// queued (e.g. the pane was just revealed to show a clicked link).
|
|
26
|
+
useEffect(() => { if (!navigate?.url) loadUrl() }, [])
|
|
27
|
+
|
|
28
|
+
// Navigate to a specific authenticated URL when requested.
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (navigate?.url) {
|
|
31
|
+
setUrl(navigate.url)
|
|
32
|
+
setKey(k => k + 1)
|
|
33
|
+
}
|
|
34
|
+
}, [navigate?.n])
|
|
19
35
|
|
|
20
36
|
// Reload webview when cwd changes
|
|
21
37
|
useEffect(() => {
|
|
@@ -22,6 +22,31 @@ function copySelectionAsEmailTable(terminal: Terminal): void {
|
|
|
22
22
|
)
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// Open a URL in the system browser, mirroring WebLinksAddon's safe default
|
|
26
|
+
// (clear the opener so the new page can't reach back into this window).
|
|
27
|
+
function openExternal(uri: string): void {
|
|
28
|
+
const win = window.open()
|
|
29
|
+
if (win) {
|
|
30
|
+
try { (win as { opener: unknown }).opener = null } catch { /* ignore */ }
|
|
31
|
+
win.location.href = uri
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Custom handler for links clicked in the terminal. ctlsurf page links are
|
|
36
|
+
// routed into the embedded (authenticated) ctlsurf panel via a window event;
|
|
37
|
+
// everything else opens in the default browser as before.
|
|
38
|
+
function handleTerminalLink(_event: MouseEvent, uri: string): void {
|
|
39
|
+
window.worker.getEmbedUrl(uri)
|
|
40
|
+
.then((embedUrl) => {
|
|
41
|
+
if (embedUrl) {
|
|
42
|
+
window.dispatchEvent(new CustomEvent('ctlsurf-open-url', { detail: { url: embedUrl } }))
|
|
43
|
+
} else {
|
|
44
|
+
openExternal(uri)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
.catch(() => openExternal(uri))
|
|
48
|
+
}
|
|
49
|
+
|
|
25
50
|
interface AgentConfig {
|
|
26
51
|
id: string
|
|
27
52
|
name: string
|
|
@@ -105,7 +130,7 @@ function getOrCreateTerminal(tabId: string, onExit: (tabId: string) => void): {
|
|
|
105
130
|
|
|
106
131
|
const fitAddon = new FitAddon()
|
|
107
132
|
terminal.loadAddon(fitAddon)
|
|
108
|
-
terminal.loadAddon(new WebLinksAddon())
|
|
133
|
+
terminal.loadAddon(new WebLinksAddon(handleTerminalLink))
|
|
109
134
|
|
|
110
135
|
// ⌘/Ctrl+Shift+E → convert the current selection to an email table. Handled
|
|
111
136
|
// here (not as a DOM listener) so it works even while a mouse-tracking TUI
|