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: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ docs/_site/
docs/.jekyll-cache/
docs/.jekyll-metadata
.vite/
.claude
16 changes: 14 additions & 2 deletions desktop/splash/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@
user-select: none;
-webkit-user-select: none;
}
/* Draggable strip under the macOS overlay title bar (the main window uses
titleBarStyle:Overlay), so the splash window can be moved while the
local server starts. Transparent and harmless on other platforms. */
.drag-strip {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 28px;
z-index: 10;
}
.wrap {
height: 100%;
display: flex;
Expand Down Expand Up @@ -62,7 +73,8 @@
</style>
</head>
<body>
<div class="wrap">
<div class="drag-strip" data-tauri-drag-region></div>
<div class="wrap" role="main" aria-label="jcode 正在启动">
<svg class="mark" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
Expand All @@ -78,7 +90,7 @@
<path fill="#ff8400" d="M 185 165 L 327 165 Q 347 165 347 185 Q 347 205 327 205 L 250 205 L 250 340 Q 250 385 222 408 Q 200 425 168 425 Q 140 425 122 412 Q 104 398 104 375 Q 104 360 114 350 Q 124 340 138 340 Q 152 340 160 349 Q 167 358 167 370 Q 167 380 162 387 Q 170 395 183 395 Q 198 395 208 382 Q 218 369 218 348 L 218 205 L 185 205 Q 165 205 165 185 Q 165 165 185 165 Z" />
</svg>
<div class="title"><b>j</b>code</div>
<div class="status"><span class="spinner"></span><span>正在启动本地服务…</span></div>
<div class="status" role="status" aria-live="polite"><span class="spinner" aria-hidden="true"></span><span>正在启动本地服务…</span></div>
</div>
</body>
</html>
10 changes: 8 additions & 2 deletions desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,18 @@ pub fn show_main(app: &AppHandle) {
}
}

/// Toggle window visibility — the tray icon's left-click behaviour.
/// Toggle window visibility — the tray icon's left-click behaviour. A minimized
/// window still reports `is_visible() == true`, so treat "visible but minimized"
/// as "should be restored" rather than hiding it; otherwise a left-click on a
/// minimized window would hide it instead of bringing it forward.
pub fn toggle_main(app: &AppHandle) {
if let Some(w) = app.get_webview_window("main") {
if w.is_visible().unwrap_or(false) {
let visible = w.is_visible().unwrap_or(false);
let minimized = w.is_minimized().unwrap_or(false);
if visible && !minimized {
let _ = w.hide();
} else {
let _ = w.unminimize();
let _ = w.show();
let _ = w.set_focus();
}
Expand Down
48 changes: 37 additions & 11 deletions internal/handler/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,23 @@ type WebHandler struct {

mu sync.Mutex
approvalCounter int
pendingApproval map[string]chan ApprovalResponse
pendingApproval map[string]*webPendingApproval

askUserMu sync.Mutex
askUserCounter int
pendingAskUser map[string]*pendingAskUser
}

// pendingApproval pairs an approval's response channel with the request payload
// so the latter can be re-surfaced to a (re)connecting client via
// /api/approval/pending — mirroring pendingAskUser. The WS approval_request event
// is fire-once, so without retaining the data a reload/reconnect while an
// approval is pending would leave the agent blocked with no card to act on.
type webPendingApproval struct {
ch chan ApprovalResponse
data WebApprovalRequestData
}

// pendingAskUser pairs a question's response channel with the request payload so
// the latter can be re-surfaced to a (re)connecting client via /api/ask/pending.
type pendingAskUser struct {
Expand All @@ -291,7 +301,7 @@ type pendingAskUser struct {
func NewWebHandler() *WebHandler {
return &WebHandler{
eventCh: make(chan WebEvent, 256),
pendingApproval: make(map[string]chan ApprovalResponse),
pendingApproval: make(map[string]*webPendingApproval),
pendingAskUser: make(map[string]*pendingAskUser),
}
}
Expand Down Expand Up @@ -391,7 +401,13 @@ func (h *WebHandler) RequestApproval(ctx context.Context, req ApprovalRequest) (
h.approvalCounter++
id := fmt.Sprintf("approval_%d", h.approvalCounter)
respCh := make(chan ApprovalResponse, 1)
h.pendingApproval[id] = respCh
data := WebApprovalRequestData{
ID: id,
ToolName: req.ToolName,
ToolArgs: req.ToolArgs,
IsExternal: req.IsExternal,
}
h.pendingApproval[id] = &webPendingApproval{ch: respCh, data: data}
h.mu.Unlock()

defer func() {
Expand All @@ -400,12 +416,7 @@ func (h *WebHandler) RequestApproval(ctx context.Context, req ApprovalRequest) (
h.mu.Unlock()
}()

h.emit("approval_request", WebApprovalRequestData{
ID: id,
ToolName: req.ToolName,
ToolArgs: req.ToolArgs,
IsExternal: req.IsExternal,
})
h.emit("approval_request", data)

select {
case resp := <-respCh:
Expand All @@ -423,7 +434,7 @@ func (h *WebHandler) RequestApproval(ctx context.Context, req ApprovalRequest) (
// Autopilot on a single Allow click.
func (h *WebHandler) ResolveApproval(id string, approved, approveAll bool) error {
h.mu.Lock()
ch, ok := h.pendingApproval[id]
p, ok := h.pendingApproval[id]
h.mu.Unlock()

if !ok {
Expand All @@ -436,13 +447,28 @@ func (h *WebHandler) ResolveApproval(id string, approved, approveAll bool) error
}

select {
case ch <- ApprovalResponse{Approved: approved, Mode: mode}:
case p.ch <- ApprovalResponse{Approved: approved, Mode: mode}:
return nil
default:
return fmt.Errorf("approval %q already resolved", id)
}
}

// PendingApprovalRequests returns the still-unanswered approval requests so a
// reloaded/reconnecting client can re-surface the approval card (the
// approval_request WS event is fire-once and ephemeral). Without this, a page
// refresh or WS reconnect while an approval is pending would drop the card and
// leave the agent blocked forever. Mirrors PendingAskUserRequests.
func (h *WebHandler) PendingApprovalRequests() []WebApprovalRequestData {
h.mu.Lock()
defer h.mu.Unlock()
out := make([]WebApprovalRequestData, 0, len(h.pendingApproval))
for _, p := range h.pendingApproval {
out = append(out, p.data)
}
return out
}

// --- Ask-user flow ---

// RequestAskUser emits the question(s) to web clients and blocks until the user
Expand Down
9 changes: 9 additions & 0 deletions internal/web/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ func (s *Server) Start(ctx context.Context) error {
mux.HandleFunc("POST /api/goal", s.handleSetGoal)
mux.HandleFunc("DELETE /api/goal", s.handleClearGoal)
mux.HandleFunc("POST /api/approval", s.handleApproval)
mux.HandleFunc("GET /api/approval/pending", s.handlePendingApproval)
mux.HandleFunc("POST /api/ask", s.handleAskUser)
mux.HandleFunc("GET /api/ask/pending", s.handlePendingAskUser)
mux.HandleFunc("GET /api/files", s.handleListFiles)
Expand Down Expand Up @@ -1267,6 +1268,14 @@ func (s *Server) handleApproval(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
}

// handlePendingApproval returns approval requests still awaiting a decision.
// The frontend pulls this after rebuilding the timeline (page reload / session
// resume / WS reconnect) so an in-flight approval is re-attached as a card
// instead of leaving the agent blocked forever.
func (s *Server) handlePendingApproval(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, s.handler.PendingApprovalRequests())
}

// handleAskUser resolves a pending ask_user request with the user's answers,
// routed back to the blocked tool via WebHandler.ResolveAskUser. The "answers"
// array is parallel to the questions the frontend received in ask_user_request:
Expand Down
Loading
Loading