Merge branch 'main' into dexter/eng-1806-preserve-markdown-syntax-in-headings-with-uniform-text-size

This commit is contained in:
dexhorthy
2025-08-05 19:29:12 -07:00
15 changed files with 324 additions and 190 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -22,6 +22,7 @@
"shell:allow-spawn",
"shell:allow-execute",
"shell:allow-kill",
"shell:allow-open",
"store:default",
"log:default"
]

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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 && (

View File

@@ -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>

View File

@@ -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 && (

View File

@@ -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()

View File

@@ -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'

View File

@@ -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

View 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 }

View File

@@ -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,

View File

@@ -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 || '',