mirror of
				https://github.com/humanlayer/humanlayer.git
				synced 2025-08-20 19:01:22 +03:00 
			
		
		
		
	Merge branch 'main' into dexter/eng-1806-preserve-markdown-syntax-in-headings-with-uniform-text-size
This commit is contained in:
		
							
								
								
									
										92
									
								
								.github/workflows/release-macos.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										92
									
								
								.github/workflows/release-macos.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,10 +1,13 @@
 | 
			
		||||
name: Build macOS Release Artifacts
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  schedule:
 | 
			
		||||
    # Run daily at 7am PT (3pm UTC during standard time, 2pm UTC during daylight saving)
 | 
			
		||||
    - cron: '0 14 * * *'
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
    inputs:
 | 
			
		||||
      release_version:
 | 
			
		||||
        description: 'Version tag for the release (defaults to YYYYMMDD)'
 | 
			
		||||
        description: 'Version tag for the release (defaults to YYYYMMDD_HHmmss for manual, YYYYMMDD-nightly for cron)'
 | 
			
		||||
        required: false
 | 
			
		||||
        type: string
 | 
			
		||||
 | 
			
		||||
@@ -19,9 +22,14 @@ jobs:
 | 
			
		||||
      - name: Set release version
 | 
			
		||||
        id: version
 | 
			
		||||
        run: |
 | 
			
		||||
          if [ -z "${{ github.event.inputs.release_version }}" ]; then
 | 
			
		||||
            echo "release_version=$(date +%Y%m%d)" >> $GITHUB_OUTPUT
 | 
			
		||||
          if [ "${{ github.event_name }}" = "schedule" ]; then
 | 
			
		||||
            # For cron/scheduled runs, use YYYYMMDD-nightly format
 | 
			
		||||
            echo "release_version=$(date +%Y%m%d)-nightly" >> $GITHUB_OUTPUT
 | 
			
		||||
          elif [ -z "${{ github.event.inputs.release_version }}" ]; then
 | 
			
		||||
            # For manual runs without specified version, use YYYYMMDD_HHmmss
 | 
			
		||||
            echo "release_version=$(date +%Y%m%d_%H%M%S)" >> $GITHUB_OUTPUT
 | 
			
		||||
          else
 | 
			
		||||
            # Use the provided version
 | 
			
		||||
            echo "release_version=${{ github.event.inputs.release_version }}" >> $GITHUB_OUTPUT
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
@@ -81,50 +89,72 @@ jobs:
 | 
			
		||||
          path: humanlayer-wui/src-tauri/target/release/bundle/dmg/*.dmg
 | 
			
		||||
          if-no-files-found: error
 | 
			
		||||
 | 
			
		||||
      - name: Upload daemon artifact
 | 
			
		||||
        uses: actions/upload-artifact@v4
 | 
			
		||||
        with:
 | 
			
		||||
          name: hld-darwin-arm64
 | 
			
		||||
          path: hld/hld-darwin-arm64
 | 
			
		||||
          if-no-files-found: error
 | 
			
		||||
 | 
			
		||||
      # Create GitHub Release with artifacts
 | 
			
		||||
      - name: Create Release
 | 
			
		||||
        if: github.event_name == 'workflow_dispatch'
 | 
			
		||||
        if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
 | 
			
		||||
        id: create_release
 | 
			
		||||
        uses: softprops/action-gh-release@v2
 | 
			
		||||
        with:
 | 
			
		||||
          tag_name: ${{ steps.version.outputs.release_version }}
 | 
			
		||||
          name: HumanLayer ${{ steps.version.outputs.release_version }} - macOS Release
 | 
			
		||||
          name: codelayer-${{ steps.version.outputs.release_version }}
 | 
			
		||||
          body: |
 | 
			
		||||
            ## HumanLayer ${{ steps.version.outputs.release_version }} - macOS Release
 | 
			
		||||
            ## codelayer-${{ steps.version.outputs.release_version }}
 | 
			
		||||
 | 
			
		||||
            This release includes:
 | 
			
		||||
            - **CodeLayer** - Desktop application (DMG installer)
 | 
			
		||||
            - **HumanLayer Daemon (hld)** - Command-line daemon (ARM64 binary)
 | 
			
		||||
 | 
			
		||||
            * **CodeLayer** - Desktop application (DMG installer)
 | 
			
		||||
 | 
			
		||||
            ### Notes
 | 
			
		||||
 | 
			
		||||
            * If you have a previous install, you will need to clean up / remove / stop previous bits (your sessions will persist!)
 | 
			
		||||
            * if your `claude` cli is not in a very-default location like `/usr/local/bin`, you will need to launch with `open /Applications/CodeLayer.app` rather than launching from Spotlight/Finder/Raycast/etc
 | 
			
		||||
 | 
			
		||||
            ### Installation Instructions
 | 
			
		||||
 | 
			
		||||
            - Install the CLI: `npm install -g humanlayer@0.11.0`
 | 
			
		||||
            - Download daemon binary (hld-darwin-arm64)
 | 
			
		||||
            - Run it in a terminal e.g. `chmod +x ~/Downloads/hld-darwin-arm64 && ~/Downloads/hld-darwin-arm64`
 | 
			
		||||
            - Dismiss the security modal
 | 
			
		||||
            - Go to System Settings > Privacy & Security and scroll to the bottom, find the "allow" button and click it
 | 
			
		||||
            - Run it again `~/Downloads/hld-darwin-arm64`
 | 
			
		||||
            - Download CodeLayer (CodeLayer.dmg)
 | 
			
		||||
            - Copy the app to Applications
 | 
			
		||||
            - Open CodeLayer with your preferred app launcher
 | 
			
		||||
            - Dismiss security modal
 | 
			
		||||
            - Go to System Settings > Privacy & Security and allow
 | 
			
		||||
            - Open CodeLayer again
 | 
			
		||||
            #### First: Cleanup
 | 
			
		||||
 | 
			
		||||
            This installation is managed as a brew cask, if you have previously set up CodeLayer manually with a separate `hld-darwin-arm64` process, you'll want to clean things up:
 | 
			
		||||
 | 
			
		||||
            * Stop all running sessions
 | 
			
		||||
              * for running sessions, ctrl+x
 | 
			
		||||
              * for sessions awaiting approval, approve or deny, then ctrl+x
 | 
			
		||||
              * sessions that have a final assistant message and are in completed/interrupted state are safe to leave as is
 | 
			
		||||
              * (right now this is necessary to preserve sessions - the latest CodeLayer includes clean shutdown/recovery for sessions)
 | 
			
		||||
            * Stop any running `hld-darwin-arm64` process
 | 
			
		||||
            * Remove any existing humanlayer cli (something like `rm $(which humanlayer)`)
 | 
			
		||||
            * Quit + remove any existing `CodeLayer.app` - `rm -r /Applications/CodeLayer.app`
 | 
			
		||||
            * Install with brew cask and the `--no-quarantine` flag - This disables macOS gatekeeper - make sure you know what you're doing!
 | 
			
		||||
 | 
			
		||||
            ```
 | 
			
		||||
            brew install --cask --no-quarantine humanlayer/humanlayer/codelayer
 | 
			
		||||
            ```
 | 
			
		||||
 | 
			
		||||
            or if you prefer tapping directly you can do
 | 
			
		||||
 | 
			
		||||
            ```
 | 
			
		||||
            brew tap humanlayer/humanlayer
 | 
			
		||||
            brew install --cask --no-quarantine codelayer
 | 
			
		||||
            ```
 | 
			
		||||
 | 
			
		||||
            Then, you can run it with
 | 
			
		||||
 | 
			
		||||
            ```
 | 
			
		||||
            open /Applications/CodeLayer.app
 | 
			
		||||
            ```
 | 
			
		||||
 | 
			
		||||
            ### Requirements
 | 
			
		||||
            - macOS (Apple Silicon/M-series)
 | 
			
		||||
            - Node.js installed
 | 
			
		||||
 | 
			
		||||
          draft: true
 | 
			
		||||
            * macOS (Apple Silicon/M-series)
 | 
			
		||||
 | 
			
		||||
            ### Troubleshooting / Known Issues
 | 
			
		||||
 | 
			
		||||
            * If install fails, ensure you've cleaned up all previous artifacts. `brew reinstall` is worth a shot as well.
 | 
			
		||||
            * Logs can be found at `~/Library/Logs/dev.humanlayer.wui/CodeLayer.log`
 | 
			
		||||
            * If daemon fails due to already running, you can `pkill hld` and reopen CodeLayer to try again
 | 
			
		||||
            * If opening from spotlight/alfred/raycast/finder fails, try `open /Applications/CodeLayer.app` to push your PATH into CodeLayer so it can better find your `claude` CLI
 | 
			
		||||
 | 
			
		||||
          draft: false
 | 
			
		||||
          prerelease: false
 | 
			
		||||
 | 
			
		||||
          files: |
 | 
			
		||||
            humanlayer-wui/src-tauri/target/release/bundle/dmg/*.dmg
 | 
			
		||||
            hld/hld-darwin-arm64
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
name=$1
 | 
			
		||||
prompt=$2
 | 
			
		||||
export ANTHROPIC_BASE_URL=https://gateway.ai.cloudflare.com/v1/de6f5660d148605859f2db08488ed418/claude_code_ralph/anthropic;
 | 
			
		||||
while :; do
 | 
			
		||||
    cat "$2" | claude -p --output-format=stream-json --verbose --dangerously-skip-permissions \
 | 
			
		||||
        | tee -a claude_output.jsonl | bun hack/visualize.ts --debug; \
 | 
			
		||||
    echo -e "===SLEEP===\n===SLEEP===\n";
 | 
			
		||||
    say "looping . . . $name";
 | 
			
		||||
    sleep 10;
 | 
			
		||||
done
 | 
			
		||||
@@ -9,7 +9,8 @@ import {
 | 
			
		||||
    CreateSessionResponse,
 | 
			
		||||
    CreateSessionResponseData,
 | 
			
		||||
    EventFromJSON,
 | 
			
		||||
    RecentPath
 | 
			
		||||
    RecentPath,
 | 
			
		||||
    ListSessionsRequest
 | 
			
		||||
} from './generated';
 | 
			
		||||
 | 
			
		||||
export interface HLDClientOptions {
 | 
			
		||||
@@ -57,7 +58,7 @@ export class HLDClient {
 | 
			
		||||
        return response.data;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async listSessions(params?: { leafOnly?: boolean; includeArchived?: boolean }): Promise<Session[]> {
 | 
			
		||||
    async listSessions(params?: ListSessionsRequest): Promise<Session[]> {
 | 
			
		||||
        const response = await this.sessionsApi.listSessions(params);
 | 
			
		||||
        return response.data;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@
 | 
			
		||||
    "shell:allow-spawn",
 | 
			
		||||
    "shell:allow-execute",
 | 
			
		||||
    "shell:allow-kill",
 | 
			
		||||
    "shell:allow-open",
 | 
			
		||||
    "store:default",
 | 
			
		||||
    "log:default"
 | 
			
		||||
  ]
 | 
			
		||||
 
 | 
			
		||||
@@ -372,6 +372,34 @@
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes slide-in-up {
 | 
			
		||||
    0% {
 | 
			
		||||
      visibility: visible;
 | 
			
		||||
      transform: translate3d(0, 100%, 0);
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
      transform: translate3d(0, 0, 0);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @keyframes slide-out-down {
 | 
			
		||||
    0% {
 | 
			
		||||
      visibility: visible;
 | 
			
		||||
      transform: translate3d(0, 0, 0);
 | 
			
		||||
    }
 | 
			
		||||
    100% {
 | 
			
		||||
      transform: translate3d(0, 100%, 0);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .animate-slide-in-up {
 | 
			
		||||
    animation: slide-in-up 1s ease-in-out 0.25s 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .animate-slide-out-down {
 | 
			
		||||
    animation: slide-out-down 1s ease-in-out 0.25s 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .animate-spin-reverse {
 | 
			
		||||
    animation: spin-reverse 3s linear infinite;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,20 @@ const groupedHotkeys = hotkeyData.reduce(
 | 
			
		||||
  {} as Record<string, typeof hotkeyData>,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export const KeyboardShortcut = ({ keyString }: { keyString: string }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <kbd
 | 
			
		||||
      className={cn(
 | 
			
		||||
        'pointer-events-none inline-flex h-5 select-none items-center gap-1',
 | 
			
		||||
        'rounded border bg-muted px-1.5 font-mono text-sm font-medium',
 | 
			
		||||
        'text-muted-foreground',
 | 
			
		||||
      )}
 | 
			
		||||
    >
 | 
			
		||||
      {keyString}
 | 
			
		||||
    </kbd>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function HotkeyPanel({ open, onOpenChange }: HotkeyPanelProps) {
 | 
			
		||||
  return (
 | 
			
		||||
    <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
 | 
			
		||||
@@ -114,15 +128,7 @@ export function HotkeyPanel({ open, onOpenChange }: HotkeyPanelProps) {
 | 
			
		||||
                        className="flex items-center justify-between"
 | 
			
		||||
                      >
 | 
			
		||||
                        <span className="text-sm">{hotkey.description}</span>
 | 
			
		||||
                        <kbd
 | 
			
		||||
                          className={cn(
 | 
			
		||||
                            'pointer-events-none inline-flex h-5 select-none items-center gap-1',
 | 
			
		||||
                            'rounded border bg-muted px-1.5 font-mono text-[10px] font-medium',
 | 
			
		||||
                            'text-muted-foreground',
 | 
			
		||||
                          )}
 | 
			
		||||
                        >
 | 
			
		||||
                          {hotkey.key}
 | 
			
		||||
                        </kbd>
 | 
			
		||||
                        <KeyboardShortcut keyString={hotkey.key} />
 | 
			
		||||
                      </CommandItem>
 | 
			
		||||
                    ))}
 | 
			
		||||
                  </CommandGroup>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { useState, useEffect } from 'react'
 | 
			
		||||
import { Outlet } from 'react-router-dom'
 | 
			
		||||
import { Outlet, useLocation } from 'react-router-dom'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
import {
 | 
			
		||||
  ApprovalResolvedEventData,
 | 
			
		||||
@@ -28,12 +28,14 @@ import { DebugPanel } from '@/components/DebugPanel'
 | 
			
		||||
import { notifyLogLocation } from '@/lib/log-notification'
 | 
			
		||||
import '@/App.css'
 | 
			
		||||
import { logger } from '@/lib/logging'
 | 
			
		||||
import { KeyboardShortcut } from '@/components/HotkeyPanel'
 | 
			
		||||
 | 
			
		||||
export function Layout() {
 | 
			
		||||
  const [approvals, setApprovals] = useState<any[]>([])
 | 
			
		||||
  const [activeSessionId] = useState<string | null>(null)
 | 
			
		||||
  const { setTheme } = useTheme()
 | 
			
		||||
  const [isDebugPanelOpen, setIsDebugPanelOpen] = useState(false)
 | 
			
		||||
  const location = useLocation()
 | 
			
		||||
 | 
			
		||||
  // Use the daemon connection hook for all connection management
 | 
			
		||||
  const { connected, connecting, version, connect } = useDaemonConnection()
 | 
			
		||||
@@ -60,6 +62,7 @@ export function Layout() {
 | 
			
		||||
  const isItemNotified = useStore(state => state.isItemNotified)
 | 
			
		||||
  const addRecentResolvedApprovalToCache = useStore(state => state.addRecentResolvedApprovalToCache)
 | 
			
		||||
  const isRecentResolvedApproval = useStore(state => state.isRecentResolvedApproval)
 | 
			
		||||
  const setActiveSessionDetail = useStore(state => state.setActiveSessionDetail)
 | 
			
		||||
 | 
			
		||||
  // Set up single SSE subscription for all events
 | 
			
		||||
  useSessionSubscriptions(connected, {
 | 
			
		||||
@@ -259,6 +262,14 @@ export function Layout() {
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (location.state?.continuationSession) {
 | 
			
		||||
      const session = location.state.continuationSession.session
 | 
			
		||||
      const conversation = location.state.continuationConversation || []
 | 
			
		||||
      setActiveSessionDetail(session.id, session, conversation)
 | 
			
		||||
    }
 | 
			
		||||
  }, [location.state?.continuationSession])
 | 
			
		||||
 | 
			
		||||
  const loadSessions = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      await useStore.getState().refreshSessions()
 | 
			
		||||
@@ -367,13 +378,15 @@ export function Layout() {
 | 
			
		||||
                href="https://github.com/humanlayer/humanlayer/issues/new?title=Feedback%20on%20CodeLayer&body=%23%23%23%20Problem%20to%20solve%20%2F%20Expected%20Behavior%0A%0A%0A%23%23%23%20Proposed%20solution"
 | 
			
		||||
                target="_blank"
 | 
			
		||||
                rel="noopener noreferrer"
 | 
			
		||||
                className="inline-flex items-center justify-center px-1.5 py-0.5 text-xs font-mono border border-border bg-background text-foreground hover:bg-accent/10 transition-colors"
 | 
			
		||||
                className="inline-flex items-center justify-center px-1.5 py-0.5 text-sm font-mono border border-border bg-background text-foreground hover:bg-accent/10 transition-colors"
 | 
			
		||||
              >
 | 
			
		||||
                <MessageCircle className="w-3 h-3" />
 | 
			
		||||
              </a>
 | 
			
		||||
            </TooltipTrigger>
 | 
			
		||||
            <TooltipContent>
 | 
			
		||||
              <p>Submit feedback (⌘⇧F)</p>
 | 
			
		||||
              <p className="flex items-center gap-1">
 | 
			
		||||
                Submit feedback <KeyboardShortcut keyString="⌘+⇧+F" />
 | 
			
		||||
              </p>
 | 
			
		||||
            </TooltipContent>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
          {import.meta.env.DEV && (
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ import {
 | 
			
		||||
import { useHotkeys, useHotkeysContext } from 'react-hotkeys-hook'
 | 
			
		||||
import { SessionTableHotkeysScope } from './internal/SessionTable'
 | 
			
		||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
 | 
			
		||||
import { KeyboardShortcut } from './HotkeyPanel'
 | 
			
		||||
 | 
			
		||||
const themes: { value: Theme; label: string; icon: React.ComponentType<{ className?: string }> }[] = [
 | 
			
		||||
  { value: 'solarized-dark', label: 'Solarized Dark', icon: Moon },
 | 
			
		||||
@@ -140,7 +141,9 @@ export function ThemeSelector() {
 | 
			
		||||
          </button>
 | 
			
		||||
        </TooltipTrigger>
 | 
			
		||||
        <TooltipContent>
 | 
			
		||||
          <p>Theme: {currentTheme?.label || 'Unknown'} (Ctrl+T)</p>
 | 
			
		||||
          <p className="flex items-center gap-1">
 | 
			
		||||
            Theme: {currentTheme?.label || 'Unknown'} <KeyboardShortcut keyString="Ctrl+T" />
 | 
			
		||||
          </p>
 | 
			
		||||
        </TooltipContent>
 | 
			
		||||
      </Tooltip>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -86,6 +86,97 @@ const ROBOT_VERBS = [
 | 
			
		||||
  'harmonizing',
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
function OmniSpinner({ randomVerb, spinnerType }: { randomVerb: string; spinnerType: number }) {
 | 
			
		||||
  // Select spinner based on random type
 | 
			
		||||
  const FancySpinner = (
 | 
			
		||||
    <div className="relative w-2 h-2">
 | 
			
		||||
      {/* Outermost orbiting particles */}
 | 
			
		||||
      <div className="absolute inset-0 animate-spin-slow">
 | 
			
		||||
        <div className="absolute top-0 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-primary/40 animate-pulse" />
 | 
			
		||||
        <div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-primary/40 animate-pulse delay-75" />
 | 
			
		||||
        <div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-1 rounded-full bg-primary/40 animate-pulse delay-150" />
 | 
			
		||||
        <div className="absolute right-0 top-1/2 -translate-y-1/2 w-1 h-1 rounded-full bg-primary/40 animate-pulse delay-300" />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {/* Outer gradient ring */}
 | 
			
		||||
      <div className="absolute inset-0 rounded-full bg-gradient-to-tr from-primary/0 via-primary/30 to-primary/0 animate-spin" />
 | 
			
		||||
 | 
			
		||||
      {/* Mid rotating ring with gradient */}
 | 
			
		||||
      <div className="absolute inset-1 rounded-full">
 | 
			
		||||
        <div className="absolute inset-0 rounded-full bg-gradient-conic from-primary/10 via-primary/50 to-primary/10 animate-spin-reverse" />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {/* Inner wave ring */}
 | 
			
		||||
      <div className="absolute inset-2 rounded-full overflow-hidden">
 | 
			
		||||
        <div className="absolute inset-0 bg-gradient-to-br from-primary/30 via-transparent to-primary/30 animate-wave" />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {/* Morphing core */}
 | 
			
		||||
      <div className="absolute inset-3 animate-morph">
 | 
			
		||||
        <div className="absolute inset-0 rounded-full bg-gradient-radial from-primary/60 to-primary/20 blur-sm" />
 | 
			
		||||
        <div className="absolute inset-0 rounded-full bg-gradient-to-br from-primary/40 to-transparent" />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {/* Center glow */}
 | 
			
		||||
      <div className="absolute inset-0 flex items-center justify-center">
 | 
			
		||||
        <div className="relative">
 | 
			
		||||
          <div className="absolute w-2 h-2 rounded-full bg-primary/80 animate-ping" />
 | 
			
		||||
          <div className="relative w-2 h-2 rounded-full bg-primary animate-pulse-bright" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {/* Random glitch effect */}
 | 
			
		||||
      <div className="absolute inset-0 rounded-full opacity-20 animate-glitch" />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const SimpleSpinner = (
 | 
			
		||||
    <div className="relative w-2 h-2">
 | 
			
		||||
      {/* Single spinning ring */}
 | 
			
		||||
      <div className="absolute inset-0 rounded-full border-2 border-primary/20 border-t-primary/60 animate-spin" />
 | 
			
		||||
 | 
			
		||||
      {/* Pulsing center dot */}
 | 
			
		||||
      <div className="absolute inset-0 flex items-center justify-center">
 | 
			
		||||
        <div className="w-2 h-2 rounded-full bg-primary/50 animate-pulse" />
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      {/* Simple gradient overlay */}
 | 
			
		||||
      <div className="absolute inset-0 rounded-full bg-gradient-to-br from-primary/10 to-transparent" />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const MinimalSpinner = (
 | 
			
		||||
    <div className="relative w-10 h-10">
 | 
			
		||||
      {/* Three dots rotating */}
 | 
			
		||||
      <div className="absolute inset-0 animate-spin">
 | 
			
		||||
        <div className="absolute top-1 left-1/2 -translate-x-1/2 w-1.5 h-1.5 rounded-full bg-primary/60" />
 | 
			
		||||
        <div className="absolute bottom-1 left-2 w-1.5 h-1.5 rounded-full bg-primary/40" />
 | 
			
		||||
        <div className="absolute bottom-1 right-2 w-1.5 h-1.5 rounded-full bg-primary/40" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const BarsSpinner = (
 | 
			
		||||
    <div className="relative w-10 h-2 flex items-center justify-center gap-1">
 | 
			
		||||
      {/* Five bouncing bars */}
 | 
			
		||||
      <div className="w-1 h-2 bg-primary/40 rounded-full animate-bounce-slow" />
 | 
			
		||||
      <div className="w-1 h-3 bg-primary/60 rounded-full animate-bounce-medium" />
 | 
			
		||||
      <div className="w-1 h-2 bg-primary/80 rounded-full animate-bounce-fast" />
 | 
			
		||||
      <div className="w-1 h-1 bg-primary/60 rounded-full animate-bounce-medium delay-150" />
 | 
			
		||||
      <div className="w-1 h-2 bg-primary/40 rounded-full animate-bounce-slow delay-300" />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  const spinners = [FancySpinner, SimpleSpinner, MinimalSpinner, BarsSpinner]
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex items-center gap-3 ">
 | 
			
		||||
      {spinners[spinnerType]}
 | 
			
		||||
      <p className="text-muted-foreground opacity-80 animate-fade-pulse">{randomVerb}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function SessionDetail({ session, onClose }: SessionDetailProps) {
 | 
			
		||||
  const [isWideView, setIsWideView] = useState(false)
 | 
			
		||||
  const [isCompactView, setIsCompactView] = useState(false)
 | 
			
		||||
@@ -132,6 +223,7 @@ function SessionDetail({ session, onClose }: SessionDetailProps) {
 | 
			
		||||
  const { shouldIgnoreMouseEvent, startKeyboardNavigation } = useKeyboardNavigationProtection()
 | 
			
		||||
 | 
			
		||||
  const isActivelyProcessing = ['starting', 'running', 'completing'].includes(session.status)
 | 
			
		||||
  // const isActivelyProcessing = true
 | 
			
		||||
  const responseInputRef = useRef<HTMLTextAreaElement>(null)
 | 
			
		||||
  const confirmingArchiveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
 | 
			
		||||
 | 
			
		||||
@@ -566,6 +658,15 @@ function SessionDetail({ session, onClose }: SessionDetailProps) {
 | 
			
		||||
    }
 | 
			
		||||
  }, [session.status, events])
 | 
			
		||||
 | 
			
		||||
  let cardVerticalPadding = isCompactView ? 'py-2' : 'py-4'
 | 
			
		||||
 | 
			
		||||
  if (isActivelyProcessing) {
 | 
			
		||||
    const cardLoadingLowerPadding = 'pb-12'
 | 
			
		||||
    cardVerticalPadding = isCompactView
 | 
			
		||||
      ? `pt-2 ${cardLoadingLowerPadding}`
 | 
			
		||||
      : `pt-4 ${cardLoadingLowerPadding}`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <section className={`flex flex-col h-full ${isCompactView ? 'gap-2' : 'gap-4'}`}>
 | 
			
		||||
      {!isCompactView && (
 | 
			
		||||
@@ -740,7 +841,7 @@ function SessionDetail({ session, onClose }: SessionDetailProps) {
 | 
			
		||||
      <div className={`flex flex-1 gap-4 ${isWideView ? 'flex-row' : 'flex-col'} min-h-0`}>
 | 
			
		||||
        {/* Conversation content and Loading */}
 | 
			
		||||
        <Card
 | 
			
		||||
          className={`${isWideView ? 'flex-1' : 'w-full'} relative ${isCompactView ? 'py-2' : 'py-4'} flex flex-col min-h-0`}
 | 
			
		||||
          className={`Conversation-Card ${isWideView ? 'flex-1' : 'w-full'} relative ${cardVerticalPadding} flex flex-col min-h-0`}
 | 
			
		||||
        >
 | 
			
		||||
          <CardContent className={`${isCompactView ? 'px-2' : 'px-4'} flex flex-col flex-1 min-h-0`}>
 | 
			
		||||
            <ConversationContent
 | 
			
		||||
@@ -767,111 +868,16 @@ function SessionDetail({ session, onClose }: SessionDetailProps) {
 | 
			
		||||
              expandedTasks={expandedTasks}
 | 
			
		||||
              toggleTaskGroup={toggleTaskGroup}
 | 
			
		||||
            />
 | 
			
		||||
            {isActivelyProcessing &&
 | 
			
		||||
              (() => {
 | 
			
		||||
                // Fancy complex spinner
 | 
			
		||||
                const fancySpinner = (
 | 
			
		||||
                  <div className="relative w-10 h-10">
 | 
			
		||||
                    {/* Outermost orbiting particles */}
 | 
			
		||||
                    <div className="absolute inset-0 animate-spin-slow">
 | 
			
		||||
                      <div className="absolute top-0 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-primary/40 animate-pulse" />
 | 
			
		||||
                      <div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-1 h-1 rounded-full bg-primary/40 animate-pulse delay-75" />
 | 
			
		||||
                      <div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-1 rounded-full bg-primary/40 animate-pulse delay-150" />
 | 
			
		||||
                      <div className="absolute right-0 top-1/2 -translate-y-1/2 w-1 h-1 rounded-full bg-primary/40 animate-pulse delay-300" />
 | 
			
		||||
          </CardContent>
 | 
			
		||||
          {isActivelyProcessing && (
 | 
			
		||||
            <div
 | 
			
		||||
              className={`absolute bottom-0 left-0 px-3 py-1.5 border-t border-border bg-secondary/30 w-full font-mono text-sm uppercase tracking-wider text-muted-foreground transition-all duration-300 ease-out ${
 | 
			
		||||
                isActivelyProcessing ? 'translate-y-0 opacity-100' : 'translate-y-full opacity-0'
 | 
			
		||||
              }`}
 | 
			
		||||
            >
 | 
			
		||||
              <OmniSpinner randomVerb={randomVerb} spinnerType={spinnerType} />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
                    {/* Outer gradient ring */}
 | 
			
		||||
                    <div className="absolute inset-0 rounded-full bg-gradient-to-tr from-primary/0 via-primary/30 to-primary/0 animate-spin" />
 | 
			
		||||
 | 
			
		||||
                    {/* Mid rotating ring with gradient */}
 | 
			
		||||
                    <div className="absolute inset-1 rounded-full">
 | 
			
		||||
                      <div className="absolute inset-0 rounded-full bg-gradient-conic from-primary/10 via-primary/50 to-primary/10 animate-spin-reverse" />
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {/* Inner wave ring */}
 | 
			
		||||
                    <div className="absolute inset-2 rounded-full overflow-hidden">
 | 
			
		||||
                      <div className="absolute inset-0 bg-gradient-to-br from-primary/30 via-transparent to-primary/30 animate-wave" />
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {/* Morphing core */}
 | 
			
		||||
                    <div className="absolute inset-3 animate-morph">
 | 
			
		||||
                      <div className="absolute inset-0 rounded-full bg-gradient-radial from-primary/60 to-primary/20 blur-sm" />
 | 
			
		||||
                      <div className="absolute inset-0 rounded-full bg-gradient-to-br from-primary/40 to-transparent" />
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {/* Center glow */}
 | 
			
		||||
                    <div className="absolute inset-0 flex items-center justify-center">
 | 
			
		||||
                      <div className="relative">
 | 
			
		||||
                        <div className="absolute w-2 h-2 rounded-full bg-primary/80 animate-ping" />
 | 
			
		||||
                        <div className="relative w-2 h-2 rounded-full bg-primary animate-pulse-bright" />
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {/* Random glitch effect */}
 | 
			
		||||
                    <div className="absolute inset-0 rounded-full opacity-20 animate-glitch" />
 | 
			
		||||
                  </div>
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                // Simple minimal spinner
 | 
			
		||||
                const simpleSpinner = (
 | 
			
		||||
                  <div className="relative w-10 h-10">
 | 
			
		||||
                    {/* Single spinning ring */}
 | 
			
		||||
                    <div className="absolute inset-0 rounded-full border-2 border-primary/20 border-t-primary/60 animate-spin" />
 | 
			
		||||
 | 
			
		||||
                    {/* Pulsing center dot */}
 | 
			
		||||
                    <div className="absolute inset-0 flex items-center justify-center">
 | 
			
		||||
                      <div className="w-2 h-2 rounded-full bg-primary/50 animate-pulse" />
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                    {/* Simple gradient overlay */}
 | 
			
		||||
                    <div className="absolute inset-0 rounded-full bg-gradient-to-br from-primary/10 to-transparent" />
 | 
			
		||||
                  </div>
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                // Ultra minimal spinner
 | 
			
		||||
                const minimalSpinner = (
 | 
			
		||||
                  <div className="relative w-10 h-10">
 | 
			
		||||
                    {/* Three dots rotating */}
 | 
			
		||||
                    <div className="absolute inset-0 animate-spin">
 | 
			
		||||
                      <div className="absolute top-1 left-1/2 -translate-x-1/2 w-1.5 h-1.5 rounded-full bg-primary/60" />
 | 
			
		||||
                      <div className="absolute bottom-1 left-2 w-1.5 h-1.5 rounded-full bg-primary/40" />
 | 
			
		||||
                      <div className="absolute bottom-1 right-2 w-1.5 h-1.5 rounded-full bg-primary/40" />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                // Bouncing bars spinner
 | 
			
		||||
                const barsSpinner = (
 | 
			
		||||
                  <div className="relative w-10 h-10 flex items-center justify-center gap-1">
 | 
			
		||||
                    {/* Five bouncing bars */}
 | 
			
		||||
                    <div className="w-1 h-6 bg-primary/40 rounded-full animate-bounce-slow" />
 | 
			
		||||
                    <div className="w-1 h-8 bg-primary/60 rounded-full animate-bounce-medium" />
 | 
			
		||||
                    <div className="w-1 h-5 bg-primary/80 rounded-full animate-bounce-fast" />
 | 
			
		||||
                    <div className="w-1 h-7 bg-primary/60 rounded-full animate-bounce-medium delay-150" />
 | 
			
		||||
                    <div className="w-1 h-4 bg-primary/40 rounded-full animate-bounce-slow delay-300" />
 | 
			
		||||
                  </div>
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
                // Select spinner based on random type
 | 
			
		||||
                const spinner =
 | 
			
		||||
                  spinnerType === 0
 | 
			
		||||
                    ? fancySpinner
 | 
			
		||||
                    : spinnerType === 1
 | 
			
		||||
                      ? simpleSpinner
 | 
			
		||||
                      : spinnerType === 2
 | 
			
		||||
                        ? minimalSpinner
 | 
			
		||||
                        : barsSpinner
 | 
			
		||||
 | 
			
		||||
                return (
 | 
			
		||||
                  <div className="flex items-center gap-3 mt-4 pl-4">
 | 
			
		||||
                    {spinner}
 | 
			
		||||
                    <p className="text-sm font-medium text-muted-foreground opacity-80 animate-fade-pulse">
 | 
			
		||||
                      {randomVerb}
 | 
			
		||||
                    </p>
 | 
			
		||||
                  </div>
 | 
			
		||||
                )
 | 
			
		||||
              })()}
 | 
			
		||||
 | 
			
		||||
          )}
 | 
			
		||||
          {/* Status bar for pending approvals */}
 | 
			
		||||
          <div
 | 
			
		||||
            className={`absolute bottom-0 left-0 right-0 p-2 cursor-pointer transition-all duration-300 ease-in-out ${
 | 
			
		||||
@@ -891,7 +897,6 @@ function SessionDetail({ session, onClose }: SessionDetailProps) {
 | 
			
		||||
              <ChevronDown className="w-3 h-3 animate-bounce" />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          </CardContent>
 | 
			
		||||
        </Card>
 | 
			
		||||
 | 
			
		||||
        {isWideView && lastTodo && (
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,7 @@ export function useSessionActions({
 | 
			
		||||
  const archiveSession = useStore(state => state.archiveSession)
 | 
			
		||||
  const setViewMode = useStore(state => state.setViewMode)
 | 
			
		||||
  const trackNavigationFrom = useStore(state => state.trackNavigationFrom)
 | 
			
		||||
  const updateActiveSessionDetail = useStore(state => state.updateActiveSessionDetail)
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
 | 
			
		||||
  // Update response input when fork message is selected
 | 
			
		||||
@@ -44,6 +45,7 @@ export function useSessionActions({
 | 
			
		||||
 | 
			
		||||
  // Continue session functionality
 | 
			
		||||
  const handleContinueSession = useCallback(async () => {
 | 
			
		||||
    const sessionConversation = useStore.getState().activeSessionDetail?.conversation
 | 
			
		||||
    if (!responseInput.trim() || isResponding) return
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
@@ -67,6 +69,14 @@ export function useSessionActions({
 | 
			
		||||
 | 
			
		||||
      const response = await daemonClient.continueSession(targetSessionId, messageToSend)
 | 
			
		||||
 | 
			
		||||
      if (!response.new_session_id) {
 | 
			
		||||
        throw new Error('No new session ID returned from continueSession')
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const nextSession = await daemonClient.getSessionState(response.new_session_id)
 | 
			
		||||
 | 
			
		||||
      updateActiveSessionDetail(nextSession.session)
 | 
			
		||||
 | 
			
		||||
      // Clear fork state
 | 
			
		||||
      setForkFromSessionId(null)
 | 
			
		||||
 | 
			
		||||
@@ -76,7 +86,12 @@ export function useSessionActions({
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Always navigate to the new session - the backend handles queuing
 | 
			
		||||
      navigate(`/sessions/${response.new_session_id || session.id}`)
 | 
			
		||||
      navigate(`/sessions/${response.new_session_id || session.id}`, {
 | 
			
		||||
        state: {
 | 
			
		||||
          continuationSession: nextSession,
 | 
			
		||||
          continuationConversation: sessionConversation,
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      // Refresh the session list to ensure UI reflects current state
 | 
			
		||||
      await refreshSessions()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,12 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
const Kbd = ({ children, className = '' }: { children: React.ReactNode; className?: string }) => (
 | 
			
		||||
  <kbd className={`px-1 py-0.5 bg-muted rounded ${className}`}>{children}</kbd>
 | 
			
		||||
)
 | 
			
		||||
export const Kbd = ({
 | 
			
		||||
  children,
 | 
			
		||||
  className = '',
 | 
			
		||||
}: {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
  className?: string
 | 
			
		||||
}) => <kbd className={`px-1 py-0.5 bg-muted rounded ${className}`}>{children}</kbd>
 | 
			
		||||
 | 
			
		||||
export const getSessionStatusText = (status: string): string => {
 | 
			
		||||
  if (status === 'completed') return 'Continue this conversation with a new message'
 | 
			
		||||
 
 | 
			
		||||
@@ -219,7 +219,7 @@ export function ConversationContent({
 | 
			
		||||
      <div
 | 
			
		||||
        ref={containerRef}
 | 
			
		||||
        data-conversation-container
 | 
			
		||||
        className="overflow-y-auto flex-1 flex flex-col justify-end"
 | 
			
		||||
        className="overflow-y-auto flex-1 flex flex-col"
 | 
			
		||||
      >
 | 
			
		||||
        <div>
 | 
			
		||||
          {nonEmptyDisplayObjects.map((displayObject, index) => (
 | 
			
		||||
@@ -333,7 +333,7 @@ export function ConversationContent({
 | 
			
		||||
    <div
 | 
			
		||||
      ref={containerRef}
 | 
			
		||||
      data-conversation-container
 | 
			
		||||
      className="overflow-y-auto flex-1 flex flex-col justify-end"
 | 
			
		||||
      className="overflow-y-auto flex-1 flex flex-col"
 | 
			
		||||
    >
 | 
			
		||||
      <div>
 | 
			
		||||
        {rootEvents
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								humanlayer-wui/src/components/ui/keyboard-shortcut.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								humanlayer-wui/src/components/ui/keyboard-shortcut.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
import * as React from 'react'
 | 
			
		||||
import { cn } from '@/lib/utils'
 | 
			
		||||
 | 
			
		||||
export interface KeyboardShortcutProps extends React.HTMLAttributes<HTMLSpanElement> {
 | 
			
		||||
  children: React.ReactNode
 | 
			
		||||
  size?: 'sm' | 'md'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const KeyboardShortcut = React.forwardRef<HTMLSpanElement, KeyboardShortcutProps>(
 | 
			
		||||
  ({ className, children, size = 'sm', ...props }, ref) => {
 | 
			
		||||
    return (
 | 
			
		||||
      <span
 | 
			
		||||
        ref={ref}
 | 
			
		||||
        className={cn(
 | 
			
		||||
          // Base styles - emulating the chicklet appearance
 | 
			
		||||
          'inline-flex items-center justify-center',
 | 
			
		||||
          'bg-transparent', // Transparent background
 | 
			
		||||
          'border border-border', // Use app's border color
 | 
			
		||||
          'rounded-md', // Slightly rounded corners
 | 
			
		||||
          'font-mono font-medium', // Clean typography
 | 
			
		||||
          'text-muted-foreground', // Use muted text color like tooltips
 | 
			
		||||
          'select-none', // Prevent text selection
 | 
			
		||||
          // Size variants
 | 
			
		||||
          size === 'sm' && 'px-1.5 py-0.5 text-xs min-w-[1.25rem] h-5',
 | 
			
		||||
          size === 'md' && 'px-2 py-1 text-sm min-w-[1.5rem] h-6',
 | 
			
		||||
          className,
 | 
			
		||||
        )}
 | 
			
		||||
        {...props}
 | 
			
		||||
      >
 | 
			
		||||
        {children}
 | 
			
		||||
      </span>
 | 
			
		||||
    )
 | 
			
		||||
  },
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
KeyboardShortcut.displayName = 'KeyboardShortcut'
 | 
			
		||||
 | 
			
		||||
export { KeyboardShortcut }
 | 
			
		||||
@@ -166,7 +166,8 @@ export class HTTPDaemonClient implements IDaemonClient {
 | 
			
		||||
    // The SDK's listSessions with leafOnly=true is equivalent
 | 
			
		||||
    const response = await this.client!.listSessions({
 | 
			
		||||
      leafOnly: true,
 | 
			
		||||
      includeArchived: request?.include_archived || request?.archived_only,
 | 
			
		||||
      includeArchived: request?.include_archived,
 | 
			
		||||
      archivedOnly: request?.archived_only,
 | 
			
		||||
    })
 | 
			
		||||
    return {
 | 
			
		||||
      sessions: response,
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,7 @@ export function SessionDetailPage() {
 | 
			
		||||
 | 
			
		||||
  // Render SessionDetail even during loading so it can show its skeleton UI
 | 
			
		||||
  // Pass a minimal session object if still loading
 | 
			
		||||
  const session = activeSessionDetail?.session?.id
 | 
			
		||||
  let session = activeSessionDetail?.session?.id
 | 
			
		||||
    ? activeSessionDetail.session
 | 
			
		||||
    : {
 | 
			
		||||
        id: sessionId || '',
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user