@sergeychuvayev/claude-fleet 0.9.0 → 0.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sergeychuvayev/claude-fleet",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "A local control room for Claude Code sessions",
5
5
  "keywords": [
6
6
  "claude",
package/public/app.js CHANGED
@@ -622,9 +622,9 @@ setInterval(() => { if (!document.hidden) tick() },2000)
622
622
  document.addEventListener('visibilitychange', () => { if (!document.hidden) tick() })
623
623
 
624
624
  // Layout the operator controls: a draggable split between the session list and the
625
- // inspector, and a resizable console. Both are remembered per browser; a storage
625
+ // inspector, and resizable session sections. Sizes are remembered per browser; a storage
626
626
  // failure (private window, blocked site data) only costs the remembered size.
627
- const LAYOUT = { split: 'fleet:split', height: 'fleet:conv-height' }
627
+ const LAYOUT = { split: 'fleet:split' }
628
628
  const SPLIT_DEFAULT = 58, LIST_MIN = 300, DETAIL_MIN = 380
629
629
 
630
630
  function applySplit(percent, { save = true } = {}) {
@@ -690,18 +690,106 @@ function initSplitter() {
690
690
  })
691
691
  }
692
692
 
693
- // The console is rebuilt whenever a different agent is selected, so its height is
694
- // restored on each build and written back when the native resize grip is released.
695
- let conversationObserver = null
693
+ // Vertical sections share the same interaction as the session-list divider.
694
+ // Observe the container so saved sizes yield when the viewport or controls change.
695
+ let panelLayoutCleanup = () => {}
696
696
  function watchConversation(element) {
697
+ panelLayoutCleanup()
697
698
  if (!element) return
698
- const saved = store.get(LAYOUT.height)
699
- if (saved) element.style.height = saved
700
- conversationObserver ||= new ResizeObserver(entries => {
701
- for (const entry of entries) if (entry.target.style.height) store.set(LAYOUT.height, entry.target.style.height)
702
- })
703
- conversationObserver.disconnect()
704
- conversationObserver.observe(element)
699
+ const container = element.parentElement
700
+ const disposers = []
701
+ const addPanel = (panel, { key, label, min, initial, before = false }) => {
702
+ const divider = document.createElement('div')
703
+ divider.className = 'panel-splitter'
704
+ divider.tabIndex = 0
705
+ divider.setAttribute('role', 'separator')
706
+ divider.setAttribute('aria-orientation', 'horizontal')
707
+ divider.setAttribute('aria-label', label)
708
+ divider.setAttribute('aria-controls', panel.id)
709
+ divider.title = 'Drag to resize · arrow keys to adjust · double-click to reset'
710
+ before ? panel.before(divider) : panel.after(divider)
711
+ const saved = Number(store.get(key))
712
+ let preferred = Number.isFinite(saved) && saved >= min ? saved : initial
713
+ let drag = null
714
+ const height = () => panel.getBoundingClientRect().height
715
+ const maximum = () => Math.max(min, Math.min(container.clientHeight * .45,
716
+ height() + element.clientHeight - 120))
717
+ const apply = () => {
718
+ const mobile = matchMedia('(max-width:720px)').matches
719
+ const collapsed = panel.tagName === 'DETAILS' && !panel.open
720
+ divider.hidden = mobile || collapsed
721
+ if (mobile || collapsed) { panel.style.removeProperty('height'); return }
722
+ const max = Math.floor(maximum())
723
+ const value = Math.round(Math.max(min, Math.min(preferred, max)))
724
+ panel.style.height = `${value}px`
725
+ divider.setAttribute('aria-valuemin', String(min))
726
+ divider.setAttribute('aria-valuemax', String(max))
727
+ divider.setAttribute('aria-valuenow', String(value))
728
+ divider.setAttribute('aria-valuetext', `${value} pixels`)
729
+ }
730
+ const set = value => {
731
+ preferred = Math.max(min, Math.min(value, maximum()))
732
+ store.set(key, String(Math.round(preferred)))
733
+ apply()
734
+ }
735
+ const reset = () => { preferred = initial; store.clear(key); apply() }
736
+ const stop = () => {
737
+ if (!drag) return
738
+ const id = drag.id
739
+ drag = null
740
+ divider.removeAttribute('data-dragging')
741
+ document.body.removeAttribute('data-panel-resizing')
742
+ if (divider.hasPointerCapture?.(id)) divider.releasePointerCapture(id)
743
+ }
744
+ divider.addEventListener('pointerdown', event => {
745
+ if (event.button) return
746
+ event.preventDefault()
747
+ divider.focus({ preventScroll: true })
748
+ drag = { y: event.clientY, height: height(), id: event.pointerId }
749
+ divider.setPointerCapture(event.pointerId)
750
+ divider.setAttribute('data-dragging', '')
751
+ document.body.setAttribute('data-panel-resizing', '')
752
+ })
753
+ divider.addEventListener('pointermove', event => {
754
+ if (drag && event.pointerId === drag.id) set(drag.height + (event.clientY - drag.y) * (before ? -1 : 1))
755
+ })
756
+ for (const event of ['pointerup', 'pointercancel', 'lostpointercapture']) divider.addEventListener(event, stop)
757
+ divider.addEventListener('dblclick', reset)
758
+ divider.addEventListener('keydown', event => {
759
+ const delta = { ArrowUp: -10, ArrowDown: 10 }[event.key]
760
+ if (delta !== undefined) { event.preventDefault(); set(height() + delta * (before ? -1 : 1)) }
761
+ else if (['Home', 'End', 'Enter', ' '].includes(event.key)) {
762
+ event.preventDefault()
763
+ if (event.key === 'Home') set(min)
764
+ else if (event.key === 'End') set(maximum())
765
+ else reset()
766
+ }
767
+ })
768
+ panel.addEventListener('toggle', apply)
769
+ const observer = new ResizeObserver(apply)
770
+ observer.observe(container)
771
+ addEventListener('resize', apply)
772
+ const dispose = () => { stop(); observer.disconnect(); removeEventListener('resize', apply); panel.removeEventListener('toggle', apply); divider.remove() }
773
+ disposers.push(dispose)
774
+ apply()
775
+ return dispose
776
+ }
777
+ const composer = $('composer')
778
+ if (composer) addPanel(composer, { key: 'fleet:composer-height', label: 'Resize message composer', min: 130, initial: 170, before: true })
779
+ let board = null, disposeBoard = null
780
+ const syncBoard = () => {
781
+ const next = $('initiative-board')
782
+ if (next === board) return
783
+ disposeBoard?.()
784
+ board = next
785
+ if (board) {
786
+ disposeBoard = addPanel(board, { key: 'fleet:overview-height', label: 'Resize team overview', min: 90, initial: 220 })
787
+ }
788
+ }
789
+ const mutation = new MutationObserver(syncBoard)
790
+ mutation.observe(container, { childList: true })
791
+ syncBoard()
792
+ panelLayoutCleanup = () => { mutation.disconnect(); disposers.forEach(dispose => dispose()) }
705
793
  }
706
794
  initSplitter()
707
795
 
package/public/control.js CHANGED
@@ -112,6 +112,7 @@ function selectControl(session) {
112
112
  const next=session?.managedId || null
113
113
  if(next===controlId && next) return
114
114
  controlId=next;controlSession=null;controlVersion++
115
+ window.Fleet.watchConversation(null)
115
116
  $('control-panel').innerHTML=''
116
117
  if(next){
117
118
  $('control-panel').innerHTML=`<div class="conversation-header"><h3 id="conversation-title">Conversation</h3><button type="button" id="close-agent" class="button close-agent" title="Remove this conversation from Fleet">Close</button><label class="mode-picker"><span class="sr-only">Model for this agent</span><select id="model-choice" title="Applies from your next message"></select></label><label class="mode-picker"><span class="sr-only">Approvals for this agent</span><select id="approval-mode"><option value="auto">Auto approvals</option><option value="ask">Ask every time</option><option value="all">Approve everything</option></select></label><span id="agent-context" class="subtle context-chip"></span><span id="agent-state" class="subtle">Connecting…</span></div><div id="conversation" class="conversation" role="log" aria-label="Agent conversation" aria-live="off"><p class="note">Loading conversation…</p></div><div id="agent-error" class="form-error" role="status" hidden></div><div id="approvals"></div><form id="composer" class="composer"><label class="sr-only" for="message-input">Message this agent</label><ul id="slash-picker" class="slash-picker" role="listbox" aria-label="Commands and skills" hidden></ul><div id="attach-tray" class="attach-tray" hidden></div><textarea id="message-input" rows="3" maxlength="16000" placeholder="What should this agent do next? · press / for commands · paste an image" role="combobox" aria-expanded="false" aria-controls="slash-picker" aria-autocomplete="list"></textarea><div class="composer-footer"><span id="composer-hint" class="note">Enter to send · Shift + Enter for a new line</span><button id="stop-agent" type="button" class="button stop" hidden>■ Stop</button><button id="send-message" class="button resume" type="submit">Send ↗</button></div><p id="send-error" class="form-error" role="alert" hidden></p></form>`
package/public/styles.css CHANGED
@@ -2592,3 +2592,54 @@ body[data-modal] { overflow:hidden }
2592
2592
  .status-fleet { margin-left:0 }
2593
2593
  .status-conn { border:0; padding-left:0 }
2594
2594
  }
2595
+
2596
+ /* Open session sections. The transcript keeps the remaining vertical space. */
2597
+ .detail:has(#composer) #control-panel { padding:14px 16px 0; overflow:hidden }
2598
+ .detail:has(#composer) .conversation {
2599
+ border:0; border-radius:0; background:transparent; padding:0;
2600
+ resize:none; flex:1 1 0; height:auto; min-height:120px;
2601
+ }
2602
+ .conversation .block {
2603
+ border:0; border-radius:0; border-bottom:1px solid var(--line);
2604
+ background:transparent; margin:0; overflow:visible;
2605
+ }
2606
+ .conversation .block-head { border:0; background:var(--panel) }
2607
+ .conversation .block[data-status="running"] .block-icon { color:var(--term-yellow) }
2608
+ .conversation .block[data-status="error"] .block-icon { color:var(--term-red) }
2609
+ #initiative-board {
2610
+ border:0; border-radius:0; margin:0; background:transparent;
2611
+ min-height:0; flex:0 1 auto;
2612
+ }
2613
+ #initiative-board[open] { display:flex; flex-direction:column }
2614
+ #initiative-board>summary { flex:none; background:transparent; padding:10px 0 }
2615
+ #initiative-board .initiative-body { min-height:0; max-height:none; overflow:auto; padding:10px 0 }
2616
+ #initiative-board .initiative-handoff { border-radius:0; background:transparent; border-left:1px solid var(--line) }
2617
+ .detail:has(#composer) .composer {
2618
+ display:flex; flex-direction:column; min-height:130px; flex:0 1 auto;
2619
+ margin:0; padding:6px 0 12px;
2620
+ }
2621
+ .composer textarea {
2622
+ flex:1 1 0; min-height:50px; border:0; border-radius:0;
2623
+ background:transparent; resize:none; padding:10px 0;
2624
+ }
2625
+ .composer textarea:focus { outline:0; box-shadow:inset 2px 0 var(--accent); padding-left:10px }
2626
+ .composer-footer { flex:none }
2627
+ .panel-splitter {
2628
+ position:relative; height:9px; flex:0 0 9px; cursor:row-resize;
2629
+ touch-action:none; margin:0 -16px;
2630
+ }
2631
+ .panel-splitter::before {
2632
+ content:""; position:absolute; left:0; right:0; top:4px;
2633
+ height:1px; background:var(--line);
2634
+ }
2635
+ .panel-splitter:hover::before, .panel-splitter:focus-visible::before,
2636
+ .panel-splitter[data-dragging]::before { height:2px; background:var(--accent) }
2637
+ .panel-splitter:focus-visible { outline:0 }
2638
+ body[data-panel-resizing], body[data-panel-resizing] * { cursor:row-resize!important; user-select:none!important }
2639
+ @media(max-width:720px) {
2640
+ .detail:has(#composer) #control-panel { overflow:visible }
2641
+ .panel-splitter { display:none }
2642
+ #initiative-board .initiative-body { max-height:32vh }
2643
+ .detail:has(#composer) .conversation { height:460px; flex:none }
2644
+ .detail:has(#composer) .composer { height:auto; min-height:170px; border-top:1px solid var(--line) }
2645
+ }