@phenx-inc/ctlsurf 0.15.2 → 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.
Files changed (32) hide show
  1. package/out/headless/index.mjs +10 -3
  2. package/out/headless/index.mjs.map +2 -2
  3. package/out/main/index.js +54 -4
  4. package/out/preload/index.js +2 -0
  5. package/out/renderer/assets/{cssMode-CPfLk04b.js → cssMode-Xx1-SozN.js} +3 -3
  6. package/out/renderer/assets/{freemarker2-CDHpuFqv.js → freemarker2-DM49yPw1.js} +1 -1
  7. package/out/renderer/assets/{handlebars-CgX9Jz36.js → handlebars-DJJBblpV.js} +1 -1
  8. package/out/renderer/assets/{html-DdgFccDG.js → html-DnMnJOxw.js} +1 -1
  9. package/out/renderer/assets/{htmlMode-CRHIwtvP.js → htmlMode-BG5u2CSH.js} +3 -3
  10. package/out/renderer/assets/{index-DVdWgw7p.js → index-D65QIRev.js} +300 -84
  11. package/out/renderer/assets/{index-3sFJEz7i.css → index-DID1LlMU.css} +88 -0
  12. package/out/renderer/assets/{javascript-CHhxL0s7.js → javascript-Dci_phmN.js} +2 -2
  13. package/out/renderer/assets/{jsonMode-Cy7-eWai.js → jsonMode-Dmp3JJSm.js} +3 -3
  14. package/out/renderer/assets/{liquid-vMkF9pfk.js → liquid-C9KVysk1.js} +1 -1
  15. package/out/renderer/assets/{lspLanguageFeatures-nAygNzuG.js → lspLanguageFeatures-IDD0-KWa.js} +1 -1
  16. package/out/renderer/assets/{mdx-C4UVz5dW.js → mdx-ascmBFXX.js} +1 -1
  17. package/out/renderer/assets/{python-BHemzN9g.js → python-DcbM4Hy5.js} +1 -1
  18. package/out/renderer/assets/{razor-DH6NPzbo.js → razor-XJROxpYK.js} +1 -1
  19. package/out/renderer/assets/{tsMode-DxBdlGZB.js → tsMode-EXbzul3n.js} +1 -1
  20. package/out/renderer/assets/{typescript-BRCOaBQ5.js → typescript-DhTtFBLt.js} +1 -1
  21. package/out/renderer/assets/{xml-DVv9gDP1.js → xml-BLfEkVl1.js} +1 -1
  22. package/out/renderer/assets/{yaml-jB-C3CdS.js → yaml-DpzsqvWa.js} +1 -1
  23. package/out/renderer/index.html +2 -2
  24. package/package.json +1 -1
  25. package/src/main/index.ts +52 -1
  26. package/src/main/orchestrator.ts +18 -3
  27. package/src/preload/index.ts +4 -0
  28. package/src/renderer/App.tsx +179 -50
  29. package/src/renderer/components/AgentPicker.tsx +61 -10
  30. package/src/renderer/components/ProjectPicker.tsx +44 -13
  31. package/src/renderer/components/TerminalPanel.tsx +34 -7
  32. 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
- spawnedAgentId: string | null
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
@@ -280,7 +281,7 @@ function getOrCreateTerminal(tabId: string, onExit: (tabId: string) => void): {
280
281
  fitAddon,
281
282
  unsubData: null,
282
283
  unsubExit: null,
283
- spawnedAgentId: null,
284
+ spawnedSessionKey: null,
284
285
  pinnedToBottom: true,
285
286
  anchorLine: null,
286
287
  userScrollPending: false,
@@ -343,6 +344,29 @@ export function focusTerminal(tabId: string): void {
343
344
  _terminals.get(tabId)?.terminal.focus()
344
345
  }
345
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
+
346
370
  export function destroyTerminal(tabId: string): void {
347
371
  const state = _terminals.get(tabId)
348
372
  if (!state) return
@@ -354,7 +378,7 @@ export function destroyTerminal(tabId: string): void {
354
378
 
355
379
  // ─── Component ───────────────────────────────────────
356
380
 
357
- export function TerminalPanel({ tabId, agent, onSpawn, onExit, isActive }: TerminalPanelProps) {
381
+ export function TerminalPanel({ tabId, agent, sessionVersion, onSpawn, onExit, isActive }: TerminalPanelProps) {
358
382
  const containerRef = useRef<HTMLDivElement>(null)
359
383
  const [menu, setMenu] = useState<{ x: number; y: number } | null>(null)
360
384
  // Snapshot panel: the ⋮ button freezes the visible screen text into a static,
@@ -513,14 +537,17 @@ export function TerminalPanel({ tabId, agent, onSpawn, onExit, isActive }: Termi
513
537
  }
514
538
  }, [tabId, onExit])
515
539
 
516
- // Spawn agent when it changes for this tab
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.
517
543
  useEffect(() => {
518
544
  if (!agent) return
519
545
  const state = _terminals.get(tabId)
520
546
  if (!state) return
521
- if (state.spawnedAgentId === agent.id) return
547
+ const sessionKey = `${agent.id}:${sessionVersion}`
548
+ if (state.spawnedSessionKey === sessionKey) return
522
549
 
523
- state.spawnedAgentId = agent.id
550
+ state.spawnedSessionKey = sessionKey
524
551
  state.terminal.clear()
525
552
 
526
553
  onSpawn(tabId, agent).then(() => {
@@ -534,7 +561,7 @@ export function TerminalPanel({ tabId, agent, onSpawn, onExit, isActive }: Termi
534
561
  }
535
562
  state.terminal.focus()
536
563
  })
537
- }, [tabId, agent, onSpawn])
564
+ }, [tabId, agent, sessionVersion, onSpawn])
538
565
 
539
566
  // Focus terminal when tab becomes active — delay fit to let the container get its layout dimensions
540
567
  useEffect(() => {
@@ -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 {