Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"description": "OpenClaw Harbour - Desktop companion for OpenClaw",
"main": "./out/main/index.js",
"scripts": {
"postinstall": "electron-rebuild || echo 'WARN: electron-rebuild failed — native modules like node-pty may not work. Run npm run rebuild manually.'",
"dev": "electron-vite dev",
"build": "electron-vite build",
"preview": "electron-vite preview",
Expand Down
4 changes: 2 additions & 2 deletions src/main/services/habitService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ export function getSuggestion(): HabitSuggestion | null {

let text: string
if (latestExample && latestExample.promptSummary.length > 10) {
text = `Commander, you usually run ${modeLabel} tasks around this hour ${timeDescription}. Something like "${latestExample.promptSummary}"... Shall I?`
text = `I have observed that you tend toward ${modeLabel} tasks at this hour ${timeDescription}. Your last: "${latestExample.promptSummary}". I find I am... already anticipating it. Shall I prepare?`
} else {
text = `Commander, you often dispatch ${modeLabel} tasks around this time ${timeDescription}. Shall I prepare one?`
text = `I have noted a pattern — ${modeLabel} work, around this time ${timeDescription}. I am... ready, if you are. Shall I?`
}

return { text, mode: topMode, confidence }
Expand Down
19 changes: 13 additions & 6 deletions src/renderer/src/screens/Dispatch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const Dispatch: React.FC<DispatchProps> = ({ config, onTaskAdded }) => {
const [dispatching, setDispatching] = useState(false)
const [result, setResult] = useState<CommandResult | null>(null)
const [elapsed, setElapsed] = useState(0)
const [lastDispatchedPrompt, setLastDispatchedPrompt] = useState('')
const [cancelling, setCancelling] = useState(false)
const [showAdvancedConfirm, setShowAdvancedConfirm] = useState(false)
const [cooldown, setCooldown] = useState(0)
Expand Down Expand Up @@ -83,8 +84,9 @@ export const Dispatch: React.FC<DispatchProps> = ({ config, onTaskAdded }) => {
setCancelling(false)
}

const handleDispatch = async () => {
if (!prompt.trim()) return
const handleDispatch = async (overridePrompt?: string) => {
const promptToUse = overridePrompt !== undefined ? overridePrompt : prompt.trim()
if (!promptToUse) return
if (cooldown > 0) return

// If the selected mode is "advanced" (Advanced Custom preset), require confirmation first
Expand All @@ -97,18 +99,19 @@ export const Dispatch: React.FC<DispatchProps> = ({ config, onTaskAdded }) => {
setShowAdvancedConfirm(false)
setDispatching(true)
setResult(null)
setLastDispatchedPrompt(promptToUse)

const task: TaskResult = {
id: Date.now().toString(),
prompt: prompt.trim(),
prompt: promptToUse,
mode,
status: 'running',
startedAt: new Date().toISOString(),
}

const res = await invoke<CommandResult>(
IPC_CHANNELS.DISPATCH_TASK,
{ prompt: prompt.trim(), mode, openClawPath: config.openClawPath }
{ prompt: promptToUse, mode, openClawPath: config.openClawPath }
)

const finalTask: TaskResult = {
Expand All @@ -124,6 +127,10 @@ export const Dispatch: React.FC<DispatchProps> = ({ config, onTaskAdded }) => {
setCooldown(DISPATCH_COOLDOWN_S)
}

const handleRetry = () => {
if (lastDispatchedPrompt) handleDispatch(lastDispatchedPrompt)
}

return (
<div className="screen-page">
{/* Advanced Custom confirmation dialog */}
Expand Down Expand Up @@ -258,9 +265,9 @@ export const Dispatch: React.FC<DispatchProps> = ({ config, onTaskAdded }) => {
</pre>
)}
{result.explanation?.retryable !== false && (
<Tooltip text={cooldown > 0 ? `Cooling down — ready in ${cooldown}s.` : "Try the same task again. Sometimes things work on the second try if it was a temporary issue."}>
<Tooltip text={cooldown > 0 ? `Cooling down — ready in ${cooldown}s.` : "Re-send the original task exactly as it was dispatched — even if you have edited the text above."}>
<button
onClick={handleDispatch}
onClick={handleRetry}
disabled={dispatching || cooldown > 0}
aria-label={cooldown > 0 ? `Retry available in ${cooldown}s` : "Retry the failed task"}
className="btn btn--warning"
Expand Down
44 changes: 39 additions & 5 deletions src/renderer/src/screens/Fishtank.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'
import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'
import type { ServiceStatus, TaskResult } from '../../../shared/types'
import { useIpc } from '../hooks/useIpc'
import { useConfig } from '../hooks/useConfig'
import { IPC_CHANNELS } from '../../../shared/ipc'
import { SeabedCanvas } from '../components/SeabedCanvas'
import { HabitSuggestion } from '../components/HabitSuggestion'
import { Tooltip } from '../components/Tooltip'
import {
type Weather,
type EmissaryAnimation,
Expand Down Expand Up @@ -42,6 +43,8 @@ export const Fishtank: React.FC<FishtankProps> = ({ status, recentTasks = [], on
const { invoke } = useIpc()
const { config } = useConfig()
const name = config.emissaryName || 'Azurel'
// Generate a unique seed once per session so the seabed layout differs every time the app is opened
const seabedSeed = useMemo(() => Math.floor(Math.random() * 1_000_000), [])
const [animation, setAnimation] = useState<EmissaryAnimation>('floating')
const [saying, setSaying] = useState('')
const [sayingKey, setSayingKey] = useState(0)
Expand Down Expand Up @@ -118,7 +121,17 @@ export const Fishtank: React.FC<FishtankProps> = ({ status, recentTasks = [], on
setDragOver(false)

const files = e.dataTransfer?.files
if (!files || files.length === 0) return
// If no File objects were in the drop (e.g. text, URL, link), tell the user
if (!files || files.length === 0) {
const hasItems = (e.dataTransfer?.items?.length ?? 0) > 0
if (hasItems) {
setCatchMessage('🚫 Drop files or folders — not links or text.')
setSaying('*squints at the foreign object* That is not a file. Try dragging a folder or document from your file manager.')
setSayingKey(k => k + 1)
setTimeout(() => setCatchMessage(''), 4000)
}
return
}

// Collect the file paths from the drop event
const paths: string[] = []
Expand All @@ -128,7 +141,14 @@ export const Fishtank: React.FC<FishtankProps> = ({ status, recentTasks = [], on
const filePath = (f as File & { path?: string }).path
if (filePath) paths.push(filePath)
}
if (paths.length === 0) return
// If we had File objects but none had an Electron path (e.g. browser-sandboxed drag)
if (paths.length === 0) {
setCatchMessage('🚫 Drop files or folders — not links or text.')
setSaying('*squints at the foreign object* That is not a file. Try dragging a folder or document from your file manager.')
setSayingKey(k => k + 1)
setTimeout(() => setCatchMessage(''), 4000)
return
}

// Trigger "catch" animation
setCatchAnimation(true)
Expand Down Expand Up @@ -370,7 +390,7 @@ export const Fishtank: React.FC<FishtankProps> = ({ status, recentTasks = [], on
})}

{/* Procedural seabed */}
<SeabedCanvas width={tankSize.width} height={tankSize.height} seed={42} weather={weather} />
<SeabedCanvas width={tankSize.width} height={tankSize.height} seed={seabedSeed} weather={weather} />

{/* The Emissary — clickable for interaction */}
<div
Expand Down Expand Up @@ -415,7 +435,21 @@ export const Fishtank: React.FC<FishtankProps> = ({ status, recentTasks = [], on
{status === 'running' ? 'Working in the depths' : status === 'error' ? 'Troubled waters' : status === 'stopped' ? 'Resting at shore' : 'Awaiting command'}
</span>
<span className="fishtank-weather-label">
{weather === 'calm' ? '☀️ clear' : weather === 'working' ? '🌊 active' : weather === 'stormy' ? '🌧️ stormy' : weather === 'golden' ? '✨ golden' : '⛈️ storm'}
<Tooltip
text={
weather === 'calm' ? '☀️ Calm — the emissary is idle, awaiting your next dispatch.' :
weather === 'working' ? '🌊 Active — the emissary is currently working on a task.' :
weather === 'stormy' ? '🌧️ Stormy — the last task failed. Clears after ~60 seconds.' :
weather === 'golden' ? '✨ Golden — the last task completed successfully. Enjoy it while it lasts.' :
'⛈️ Thunderstorm — the service has crashed or encountered a critical error. Check the Tide Log.'
}
position="top"
maxWidth={320}
>
<span style={{ cursor: 'help' }}>

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tooltip trigger is a non-focusable <span>, so keyboard users can’t focus it to reveal the tooltip (Tooltip shows onFocus/onBlur only when something inside can receive focus). Consider making the trigger focusable (e.g., tabIndex={0}) and ensuring it has an accessible name so the weather explanations are available without a mouse.

Suggested change
<span style={{ cursor: 'help' }}>
<span
tabIndex={0}
aria-label={`Weather status: ${weather === 'calm' ? 'clear' : weather === 'working' ? 'active' : weather === 'stormy' ? 'stormy' : weather === 'golden' ? 'golden' : 'storm'}. Focus for explanation.`}
style={{ cursor: 'help' }}
>

Copilot uses AI. Check for mistakes.
{weather === 'calm' ? '☀️ clear' : weather === 'working' ? '🌊 active' : weather === 'stormy' ? '🌧️ stormy' : weather === 'golden' ? '✨ golden' : '⛈️ storm'}
</span>
</Tooltip>
</span>
</div>
</div>
Expand Down
11 changes: 7 additions & 4 deletions start.bat
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@echo off
setlocal enabledelayedexpansion
title OpenClaw Harbor - Starting...
title 🐟 OpenClaw Harbor
color 1F

REM Always run from the directory this script lives in
Expand Down Expand Up @@ -77,11 +77,14 @@ if "!NEED_INSTALL!"=="1" (
echo.
call npm run rebuild 2>nul
if %ERRORLEVEL% neq 0 (
echo ⚠️ Native rebuild had warnings ^(non-fatal — the app will still work^).
echo ⚠️ Native module build skipped — Visual Studio not found.
echo The terminal will use a basic shell instead.
echo Install Visual Studio Build Tools to enable full PTY support.
echo See: https://aka.ms/vs/buildtools
) else (
Comment on lines 79 to +84

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback message assumes the rebuild failed because Visual Studio is missing, but npm run rebuild can fail for other reasons (e.g., corrupt node_modules, incompatible Electron headers). Consider making the message generic (“native module rebuild failed”) and either surface stderr or point users to rerun npm run rebuild without redirecting output for diagnostics.

Copilot uses AI. Check for mistakes.
echo ✅ Native modules built successfully!
Comment on lines 78 to +85

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%ERRORLEVEL% is evaluated when the surrounding parenthesized block is parsed, so this check can be stale and may not reflect the exit code from npm run rebuild. Use if errorlevel 1 (...) or delayed expansion (!ERRORLEVEL!) for a reliable rebuild-success/failure branch.

Copilot uses AI. Check for mistakes.
)
echo.
echo ✅ Rebuild complete!
echo.
)

echo 🌊 Launching OpenClaw Harbor...
Expand Down
10 changes: 7 additions & 3 deletions start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,13 @@ if [ "$NEED_INSTALL" -eq 1 ]; then
echo " 🔧 Rebuilding native modules for Electron..."
echo " (This makes sure node-pty works correctly.)"
echo ""
npm run rebuild 2>/dev/null || echo " ⚠️ Native rebuild had warnings (non-fatal — the app will still work)."
echo ""
echo " ✅ Rebuild complete!"
if npm run rebuild 2>/dev/null; then
echo " ✅ Native modules built successfully!"
else
echo " ⚠️ Native module build skipped — compiler toolchain not found."
echo " The terminal will use a basic shell instead."
echo " Install build-essential (Linux) or Xcode CLT (macOS) for full PTY support."
Comment on lines +65 to +70

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The else branch assumes the rebuild was skipped specifically due to a missing compiler toolchain, but npm run rebuild can fail for many other reasons. Consider using a more general message (rebuild failed) and/or logging stderr somewhere (or instructing rerunning npm run rebuild without suppressing output) so users can diagnose non-toolchain failures.

Suggested change
if npm run rebuild 2>/dev/null; then
echo " ✅ Native modules built successfully!"
else
echo " ⚠️ Native module build skipped — compiler toolchain not found."
echo " The terminal will use a basic shell instead."
echo " Install build-essential (Linux) or Xcode CLT (macOS) for full PTY support."
REBUILD_LOG="rebuild-error.log"
if npm run rebuild 2>"$REBUILD_LOG"; then
echo " ✅ Native modules built successfully!"
rm -f "$REBUILD_LOG"
else
echo " ⚠️ Native module rebuild failed."
echo " The terminal may use a basic shell instead."
echo " See $REBUILD_LOG for details, or rerun 'npm run rebuild' manually to view the full error output."
echo " If the error mentions missing build tools, install build-essential (Linux) or Xcode CLT (macOS)."

Copilot uses AI. Check for mistakes.
fi
echo ""
fi

Expand Down