mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 04:36:24 +00:00
fix(mise-init): unblock the bootstrap agent + surface live agent output
The mise-init agent wedged on launch and the UI reported it green. Three distinct problems, fixed together: 1. Onboarding wedge (the proximate bug). A freshly-installed claude opens interactive setup — theme picker, then a folder-trust prompt — before the REPL. A detached tmux agent has no one to answer it, so it sat on the theme picker forever while agents.status said 'working'. New control.claude_config.ensure_onboarded() marks onboarding complete and trusts the working dir in ~/.claude.json (merge-only, so the login flow's oauthAccount survives); spawn() calls it before launching. 2. Config-name gate. control/mise.py only recognized `.mise.toml`, so a repo shipping `mise.toml` (no dot) — or config under `.config/mise/` — failed the [tasks.test] gate even when healthy. It now accepts the filenames mise itself reads and scans them all for the test task. 3. "Done" != done (the design gap). A spawned agent's real state lives in its tmux pane, but the socket is control-container-only, so the API couldn't see it. The worker now snapshots each working agent's pane tail (last ~40 lines) into two new agents columns (last_output, output_at, migration 0007) on its existing poll loop; the API serializes them and AgentsSection renders a live-output <pre> under each running agent. A wedged agent now shows the theme picker instead of a misleading green badge. Tests: home-dir writes are isolated to tmp in conftest; added coverage for claude_config seeding/merge, the mise filename set, the worker capture (including dead-session skip), and the API serialization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BxKY28XKCM6o4ag3nmaVsZ
This commit is contained in:
@@ -2,10 +2,10 @@
|
||||
* Spawning enqueues a control command that the worker turns into a tmux + claude process. */
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
import { useDashboard } from "@/components/store";
|
||||
import { Badge, Button, Card, Input, Select, StatusBadge, Textarea } from "@/components/ui";
|
||||
import { fmtFull } from "@/lib/format";
|
||||
import { fmtFull, timeAgo } from "@/lib/format";
|
||||
|
||||
const ROLE_OPTS = [
|
||||
{ value: "", label: "Role — none" },
|
||||
@@ -121,28 +121,55 @@ export function AgentsSection() {
|
||||
</thead>
|
||||
<tbody>
|
||||
{agents.map((a) => (
|
||||
<tr key={a.id}>
|
||||
<td className="mono">{a.name}</td>
|
||||
<td>{a.role ? <Badge tone="info">{a.role}</Badge> : "—"}</td>
|
||||
<td>
|
||||
<StatusBadge status={a.status} />
|
||||
</td>
|
||||
<td className="mono faint">{a.working_dir}</td>
|
||||
<td className="faint nowrap">{fmtFull(a.created_at)}</td>
|
||||
<td className="nowrap">
|
||||
<div className="hstack">
|
||||
<Button size="sm" variant="ghost" onClick={() => s.selectRun(a.project_id, a.name)}>
|
||||
Open
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => s.killAgent(a.project_id, a.name)}>
|
||||
Kill
|
||||
</Button>
|
||||
<Button size="sm" variant="danger" onClick={() => s.deleteAgent(a.project_id, a.name)}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<Fragment key={a.id}>
|
||||
<tr>
|
||||
<td className="mono">{a.name}</td>
|
||||
<td>{a.role ? <Badge tone="info">{a.role}</Badge> : "—"}</td>
|
||||
<td>
|
||||
<StatusBadge status={a.status} />
|
||||
</td>
|
||||
<td className="mono faint">{a.working_dir}</td>
|
||||
<td className="faint nowrap">{fmtFull(a.created_at)}</td>
|
||||
<td className="nowrap">
|
||||
<div className="hstack">
|
||||
<Button size="sm" variant="ghost" onClick={() => s.selectRun(a.project_id, a.name)}>
|
||||
Open
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => s.killAgent(a.project_id, a.name)}>
|
||||
Kill
|
||||
</Button>
|
||||
<Button size="sm" variant="danger" onClick={() => s.deleteAgent(a.project_id, a.name)}>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{a.status === "working" && a.last_output?.trim() && (
|
||||
<tr>
|
||||
<td colSpan={6} style={{ paddingTop: 0 }}>
|
||||
<div className="faint" style={{ fontSize: "var(--text-xs)", marginBottom: 4 }}>
|
||||
live output{a.output_at ? ` · ${timeAgo(a.output_at)}` : ""}
|
||||
</div>
|
||||
<pre
|
||||
className="mono"
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: "8px 10px",
|
||||
background: "var(--surface-2, rgba(0,0,0,0.25))",
|
||||
borderRadius: 6,
|
||||
fontSize: "var(--text-xs)",
|
||||
lineHeight: 1.4,
|
||||
maxHeight: 220,
|
||||
overflow: "auto",
|
||||
whiteSpace: "pre",
|
||||
}}
|
||||
>
|
||||
{a.last_output}
|
||||
</pre>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -30,6 +30,10 @@ export interface Agent {
|
||||
working_dir: string;
|
||||
status: string;
|
||||
role?: string | null;
|
||||
/* Latest tmux pane-tail snapshot from the worker, so the UI can show what a running
|
||||
* agent is actually doing (and expose one wedged on an interactive prompt). */
|
||||
last_output?: string | null;
|
||||
output_at?: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user