@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.
Files changed (29) hide show
  1. package/out/headless/index.mjs +36 -18
  2. package/out/headless/index.mjs.map +2 -2
  3. package/out/main/index.js +35 -6
  4. package/out/preload/index.js +3 -0
  5. package/out/renderer/assets/{cssMode-DkmdBgO7.js → cssMode-D9-xaWSI.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-CI-gkP-3.js → freemarker2-CoRAVxnv.js} +1 -1
  7. package/out/renderer/assets/{handlebars-D5tEqanR.js → handlebars-B0p9Wgkw.js} +1 -1
  8. package/out/renderer/assets/{html-fH93EYfn.js → html-D_XFJJtO.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-CRicxcwK.js → htmlMode-naWw6PWr.js} +3 -3
  10. package/out/renderer/assets/{index-BOOvUI7u.js → index-DBt_rov1.js} +64 -26
  11. package/out/renderer/assets/{javascript-D1Baz4fV.js → javascript-DDLsFUr-.js} +2 -2
  12. package/out/renderer/assets/{jsonMode-Bquqf3QN.js → jsonMode-Ixhcm5I6.js} +3 -3
  13. package/out/renderer/assets/{liquid-ByOcPjBF.js → liquid-BHgSYEHk.js} +1 -1
  14. package/out/renderer/assets/{lspLanguageFeatures-BxPLl0yy.js → lspLanguageFeatures-ClbEdD0U.js} +1 -1
  15. package/out/renderer/assets/{mdx-yuNgx0rM.js → mdx-DMngMjHR.js} +1 -1
  16. package/out/renderer/assets/{python-2OakgLlA.js → python-D_czoeY2.js} +1 -1
  17. package/out/renderer/assets/{razor-DnIVMSwa.js → razor-CLMDGvL7.js} +1 -1
  18. package/out/renderer/assets/{tsMode-CRIrHuii.js → tsMode-EIuSGG42.js} +1 -1
  19. package/out/renderer/assets/{typescript-DJ3C8Yly.js → typescript-DQkV4kKA.js} +1 -1
  20. package/out/renderer/assets/{xml-CalvD5_C.js → xml-DJ0OOQTu.js} +1 -1
  21. package/out/renderer/assets/{yaml-Cgs8pdVp.js → yaml-DxX26XLN.js} +1 -1
  22. package/out/renderer/index.html +1 -1
  23. package/package.json +1 -1
  24. package/src/main/index.ts +42 -2
  25. package/src/main/tui.ts +39 -17
  26. package/src/preload/index.ts +4 -0
  27. package/src/renderer/App.tsx +21 -1
  28. package/src/renderer/components/CtlsurfPanel.tsx +19 -3
  29. 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
- const checkbox = trackTime ? `${FG_GREEN}[✓]${RESET}${BG_MODAL}` : `${FG_DIM}[ ]${RESET}${BG_MODAL}`
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
- const logCheckbox = logChat ? `${FG_GREEN}[\u2713]${RESET}${BG_MODAL}` : `${FG_DIM}[ ]${RESET}${BG_MODAL}`
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 \u00B7 t track \u00B7 l log \u00B7 q quit'
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 + agents.length) % agents.length
245
+ selected = (selected - 1 + itemCount) % itemCount
229
246
  drawModal()
230
247
  } else if (key === '\x1b[B' || key === 'j') {
231
- selected = (selected + 1) % agents.length
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
- cleanup()
241
- resolve({ agentIdx: selected, trackTime, logChat })
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`)
@@ -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)
@@ -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
- export function CtlsurfPanel() {
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
- useEffect(() => { loadUrl() }, [])
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