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,131 +868,35 @@ 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" />
|
||||
</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 ${
|
||||
hasPendingApprovalsOutOfView
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-full pointer-events-none'
|
||||
}`}
|
||||
onClick={() => {
|
||||
const container = document.querySelector('[data-conversation-container]')
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1 font-mono text-xs uppercase tracking-wider text-muted-foreground bg-background/60 backdrop-blur-sm border-t border-border/50 py-1 shadow-sm hover:bg-background/80 transition-colors">
|
||||
<span>Pending Approval</span>
|
||||
<ChevronDown className="w-3 h-3 animate-bounce" />
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
)}
|
||||
{/* 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 ${
|
||||
hasPendingApprovalsOutOfView
|
||||
? 'opacity-100 translate-y-0'
|
||||
: 'opacity-0 translate-y-full pointer-events-none'
|
||||
}`}
|
||||
onClick={() => {
|
||||
const container = document.querySelector('[data-conversation-container]')
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-center gap-1 font-mono text-xs uppercase tracking-wider text-muted-foreground bg-background/60 backdrop-blur-sm border-t border-border/50 py-1 shadow-sm hover:bg-background/80 transition-colors">
|
||||
<span>Pending Approval</span>
|
||||
<ChevronDown className="w-3 h-3 animate-bounce" />
|
||||
</div>
|
||||
</div>
|
||||
</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