@phenx-inc/ctlsurf 0.15.1 → 0.16.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/out/headless/index.mjs +10 -3
- package/out/headless/index.mjs.map +2 -2
- package/out/main/index.js +54 -4
- package/out/preload/index.js +2 -0
- package/out/renderer/assets/{cssMode-B77M9HlN.js → cssMode-Xx1-SozN.js} +3 -3
- package/out/renderer/assets/{freemarker2-oUT2zvyQ.js → freemarker2-DM49yPw1.js} +1 -1
- package/out/renderer/assets/{handlebars-jCckoi5j.js → handlebars-DJJBblpV.js} +1 -1
- package/out/renderer/assets/{html-kAgrAaKW.js → html-DnMnJOxw.js} +1 -1
- package/out/renderer/assets/{htmlMode-DIWkIawy.js → htmlMode-BG5u2CSH.js} +3 -3
- package/out/renderer/assets/{index-CN3UN7Tm.js → index-D65QIRev.js} +318 -95
- package/out/renderer/assets/{index-3sFJEz7i.css → index-DID1LlMU.css} +88 -0
- package/out/renderer/assets/{javascript-B9GYUE-I.js → javascript-Dci_phmN.js} +2 -2
- package/out/renderer/assets/{jsonMode-BzFKTZjJ.js → jsonMode-Dmp3JJSm.js} +3 -3
- package/out/renderer/assets/{liquid-Ccw-S_Y6.js → liquid-C9KVysk1.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-BE0g7ubH.js → lspLanguageFeatures-IDD0-KWa.js} +1 -1
- package/out/renderer/assets/{mdx-oXeAA0qU.js → mdx-ascmBFXX.js} +1 -1
- package/out/renderer/assets/{python-DlSX3TvA.js → python-DcbM4Hy5.js} +1 -1
- package/out/renderer/assets/{razor-ButO9S7r.js → razor-XJROxpYK.js} +1 -1
- package/out/renderer/assets/{tsMode-CTCpE-67.js → tsMode-EXbzul3n.js} +1 -1
- package/out/renderer/assets/{typescript-9o62WOnS.js → typescript-DhTtFBLt.js} +1 -1
- package/out/renderer/assets/{xml-C4nOH2_D.js → xml-BLfEkVl1.js} +1 -1
- package/out/renderer/assets/{yaml-mXYgKoWW.js → yaml-DpzsqvWa.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/src/main/index.ts +52 -1
- package/src/main/orchestrator.ts +18 -3
- package/src/preload/index.ts +4 -0
- package/src/renderer/App.tsx +179 -50
- package/src/renderer/components/AgentPicker.tsx +61 -10
- package/src/renderer/components/FloatingMic.tsx +15 -11
- package/src/renderer/components/ProjectPicker.tsx +44 -13
- package/src/renderer/components/TerminalPanel.tsx +45 -9
- package/src/renderer/styles.css +88 -0
|
@@ -104,6 +104,7 @@ interface AgentConfig {
|
|
|
104
104
|
interface TerminalPanelProps {
|
|
105
105
|
tabId: string
|
|
106
106
|
agent: AgentConfig | null
|
|
107
|
+
sessionVersion: number
|
|
107
108
|
onSpawn: (tabId: string, agent: AgentConfig) => Promise<void>
|
|
108
109
|
onExit: (tabId: string) => void
|
|
109
110
|
isActive: boolean
|
|
@@ -118,7 +119,7 @@ type TermState = {
|
|
|
118
119
|
fitAddon: FitAddon
|
|
119
120
|
unsubData: (() => void) | null
|
|
120
121
|
unsubExit: (() => void) | null
|
|
121
|
-
|
|
122
|
+
spawnedSessionKey: string | null
|
|
122
123
|
// Follow mode: while true, new output keeps the view pinned to the bottom.
|
|
123
124
|
pinnedToBottom: boolean
|
|
124
125
|
// When NOT following, the absolute buffer line held at the top of the
|
|
@@ -197,6 +198,11 @@ function getOrCreateTerminal(tabId: string, onExit: (tabId: string) => void): {
|
|
|
197
198
|
|
|
198
199
|
const terminal = new Terminal({
|
|
199
200
|
cursorBlink: true,
|
|
201
|
+
// Codex and other modern CLIs often render links as OSC 8 hyperlinks
|
|
202
|
+
// instead of plain URL text. OSC 8 links are handled by xterm core (not
|
|
203
|
+
// WebLinksAddon), whose default activation calls window.open(). Route them
|
|
204
|
+
// through the same embedded-panel handler as detected plain-text URLs.
|
|
205
|
+
linkHandler: { activate: handleTerminalLink },
|
|
200
206
|
// Off by default on macOS; without this a right-click would reselect the
|
|
201
207
|
// word under the cursor and clobber a multi-line table selection.
|
|
202
208
|
rightClickSelectsWord: false,
|
|
@@ -275,7 +281,7 @@ function getOrCreateTerminal(tabId: string, onExit: (tabId: string) => void): {
|
|
|
275
281
|
fitAddon,
|
|
276
282
|
unsubData: null,
|
|
277
283
|
unsubExit: null,
|
|
278
|
-
|
|
284
|
+
spawnedSessionKey: null,
|
|
279
285
|
pinnedToBottom: true,
|
|
280
286
|
anchorLine: null,
|
|
281
287
|
userScrollPending: false,
|
|
@@ -338,6 +344,29 @@ export function focusTerminal(tabId: string): void {
|
|
|
338
344
|
_terminals.get(tabId)?.terminal.focus()
|
|
339
345
|
}
|
|
340
346
|
|
|
347
|
+
// Capture the rendered transcript before a live session is replaced. This is
|
|
348
|
+
// intentionally sourced from the terminal the user actually saw, so it also
|
|
349
|
+
// works for shells, custom agents, and remote SSH sessions whose native JSONL
|
|
350
|
+
// transcripts are unavailable on this machine.
|
|
351
|
+
const MAX_HANDOFF_CHARS = 1_000_000
|
|
352
|
+
export function captureTerminalSession(tabId: string): string {
|
|
353
|
+
const terminal = _terminals.get(tabId)?.terminal
|
|
354
|
+
if (!terminal) return ''
|
|
355
|
+
const buffer = terminal.buffer.active
|
|
356
|
+
const lines: string[] = []
|
|
357
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
358
|
+
const line = buffer.getLine(i)
|
|
359
|
+
if (!line) continue
|
|
360
|
+
const text = line.translateToString(true)
|
|
361
|
+
if (line.isWrapped && lines.length > 0) lines[lines.length - 1] += text
|
|
362
|
+
else lines.push(text)
|
|
363
|
+
}
|
|
364
|
+
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop()
|
|
365
|
+
const text = lines.join('\n').trim()
|
|
366
|
+
if (text.length <= MAX_HANDOFF_CHARS) return text
|
|
367
|
+
return `[Earlier terminal output omitted; showing the last ${MAX_HANDOFF_CHARS.toLocaleString()} characters.]\n${text.slice(-MAX_HANDOFF_CHARS)}`
|
|
368
|
+
}
|
|
369
|
+
|
|
341
370
|
export function destroyTerminal(tabId: string): void {
|
|
342
371
|
const state = _terminals.get(tabId)
|
|
343
372
|
if (!state) return
|
|
@@ -349,7 +378,7 @@ export function destroyTerminal(tabId: string): void {
|
|
|
349
378
|
|
|
350
379
|
// ─── Component ───────────────────────────────────────
|
|
351
380
|
|
|
352
|
-
export function TerminalPanel({ tabId, agent, onSpawn, onExit, isActive }: TerminalPanelProps) {
|
|
381
|
+
export function TerminalPanel({ tabId, agent, sessionVersion, onSpawn, onExit, isActive }: TerminalPanelProps) {
|
|
353
382
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
354
383
|
const [menu, setMenu] = useState<{ x: number; y: number } | null>(null)
|
|
355
384
|
// Snapshot panel: the ⋮ button freezes the visible screen text into a static,
|
|
@@ -469,8 +498,12 @@ export function TerminalPanel({ tabId, agent, onSpawn, onExit, isActive }: Termi
|
|
|
469
498
|
|
|
470
499
|
// Wheel over the terminal is an explicit user scroll — stamp it so onScroll
|
|
471
500
|
// treats the resulting move as intentional (not an involuntary redraw jump).
|
|
501
|
+
// Capture is important: xterm handles wheel events on a descendant and
|
|
502
|
+
// emits onScroll synchronously. A bubbling listener runs too late, causing
|
|
503
|
+
// streamed Codex output to mistake the wheel scroll for a redraw and snap
|
|
504
|
+
// the viewport straight back to the bottom.
|
|
472
505
|
const onWheel = () => markGesture(tabId)
|
|
473
|
-
terminal.element?.addEventListener('wheel', onWheel, { passive: true })
|
|
506
|
+
terminal.element?.addEventListener('wheel', onWheel, { capture: true, passive: true })
|
|
474
507
|
|
|
475
508
|
// Resize handling — skip when the container is hidden (display:none → 0x0),
|
|
476
509
|
// otherwise fit() reports a tiny size and the PTY gets stuck at ~4 cols
|
|
@@ -499,19 +532,22 @@ export function TerminalPanel({ tabId, agent, onSpawn, onExit, isActive }: Termi
|
|
|
499
532
|
clearTimeout(resizeTimeout)
|
|
500
533
|
window.removeEventListener('resize', handleResize)
|
|
501
534
|
observer.disconnect()
|
|
502
|
-
terminal.element?.removeEventListener('wheel', onWheel)
|
|
535
|
+
terminal.element?.removeEventListener('wheel', onWheel, true)
|
|
503
536
|
// Do NOT dispose terminal or unsub PTY listeners — they persist across remounts
|
|
504
537
|
}
|
|
505
538
|
}, [tabId, onExit])
|
|
506
539
|
|
|
507
|
-
// Spawn agent
|
|
540
|
+
// Spawn whenever the selected agent or session version changes. The version
|
|
541
|
+
// makes choosing a new directory (or explicitly restarting the same agent)
|
|
542
|
+
// observable even though the agent id itself did not change.
|
|
508
543
|
useEffect(() => {
|
|
509
544
|
if (!agent) return
|
|
510
545
|
const state = _terminals.get(tabId)
|
|
511
546
|
if (!state) return
|
|
512
|
-
|
|
547
|
+
const sessionKey = `${agent.id}:${sessionVersion}`
|
|
548
|
+
if (state.spawnedSessionKey === sessionKey) return
|
|
513
549
|
|
|
514
|
-
state.
|
|
550
|
+
state.spawnedSessionKey = sessionKey
|
|
515
551
|
state.terminal.clear()
|
|
516
552
|
|
|
517
553
|
onSpawn(tabId, agent).then(() => {
|
|
@@ -525,7 +561,7 @@ export function TerminalPanel({ tabId, agent, onSpawn, onExit, isActive }: Termi
|
|
|
525
561
|
}
|
|
526
562
|
state.terminal.focus()
|
|
527
563
|
})
|
|
528
|
-
}, [tabId, agent, onSpawn])
|
|
564
|
+
}, [tabId, agent, sessionVersion, onSpawn])
|
|
529
565
|
|
|
530
566
|
// Focus terminal when tab becomes active — delay fit to let the container get its layout dimensions
|
|
531
567
|
useEffect(() => {
|
package/src/renderer/styles.css
CHANGED
|
@@ -1604,6 +1604,73 @@ html, body, #root {
|
|
|
1604
1604
|
letter-spacing: 1px;
|
|
1605
1605
|
}
|
|
1606
1606
|
|
|
1607
|
+
.agent-picker-warning {
|
|
1608
|
+
margin: -2px 0 14px;
|
|
1609
|
+
padding: 9px 11px;
|
|
1610
|
+
border: 1px solid color-mix(in srgb, var(--warning) 45%, transparent);
|
|
1611
|
+
border-radius: 7px;
|
|
1612
|
+
background: color-mix(in srgb, var(--warning) 9%, transparent);
|
|
1613
|
+
color: var(--text-muted);
|
|
1614
|
+
font-size: 11px;
|
|
1615
|
+
line-height: 1.45;
|
|
1616
|
+
text-align: left;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
.agent-handoff-options {
|
|
1620
|
+
display: grid;
|
|
1621
|
+
gap: 7px;
|
|
1622
|
+
margin: 0 0 14px;
|
|
1623
|
+
padding: 0;
|
|
1624
|
+
border: 0;
|
|
1625
|
+
text-align: left;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
.agent-handoff-options legend {
|
|
1629
|
+
margin-bottom: 7px;
|
|
1630
|
+
color: var(--text-muted);
|
|
1631
|
+
font-size: 10px;
|
|
1632
|
+
letter-spacing: 0.8px;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
.agent-handoff-options label {
|
|
1636
|
+
display: flex;
|
|
1637
|
+
align-items: flex-start;
|
|
1638
|
+
gap: 9px;
|
|
1639
|
+
padding: 9px 10px;
|
|
1640
|
+
border: 1px solid var(--border-subtle);
|
|
1641
|
+
border-radius: 7px;
|
|
1642
|
+
background: var(--bg-chrome);
|
|
1643
|
+
cursor: pointer;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
.agent-handoff-options label.selected {
|
|
1647
|
+
border-color: var(--accent);
|
|
1648
|
+
background: var(--bg-chrome-hi);
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
.agent-handoff-options input {
|
|
1652
|
+
margin-top: 2px;
|
|
1653
|
+
accent-color: var(--accent);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
.agent-handoff-options span,
|
|
1657
|
+
.agent-handoff-options strong,
|
|
1658
|
+
.agent-handoff-options small {
|
|
1659
|
+
display: block;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
.agent-handoff-options strong {
|
|
1663
|
+
color: var(--text-bright);
|
|
1664
|
+
font-size: 12px;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
.agent-handoff-options small {
|
|
1668
|
+
margin-top: 2px;
|
|
1669
|
+
color: var(--text-muted);
|
|
1670
|
+
font-size: 10px;
|
|
1671
|
+
line-height: 1.35;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1607
1674
|
.agent-picker-list {
|
|
1608
1675
|
display: flex;
|
|
1609
1676
|
flex-direction: column;
|
|
@@ -1664,6 +1731,27 @@ html, body, #root {
|
|
|
1664
1731
|
accent-color: var(--accent);
|
|
1665
1732
|
}
|
|
1666
1733
|
|
|
1734
|
+
.project-notify-option {
|
|
1735
|
+
align-items: flex-start;
|
|
1736
|
+
text-align: left;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
.project-notify-option input {
|
|
1740
|
+
margin-top: 2px;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
.project-notify-option span,
|
|
1744
|
+
.project-notify-option small {
|
|
1745
|
+
display: block;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
.project-notify-option small {
|
|
1749
|
+
margin-top: 2px;
|
|
1750
|
+
color: var(--text-muted);
|
|
1751
|
+
font-size: 10px;
|
|
1752
|
+
line-height: 1.35;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1667
1755
|
/* ─── Remote (SSH) connect ───────────────────────── */
|
|
1668
1756
|
|
|
1669
1757
|
.remote-section-label {
|