Skip to content
Draft
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
4 changes: 4 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Changed

- Changed Agent Hub to default to running-agent visibility, archive parked and aborted agents, and expose live runtime metadata with structured activity ([#5251](https://github.com/can1357/oh-my-pi/pull/5251) by [@wolfiesch](https://github.com/wolfiesch)).

## [16.4.6] - 2026-07-12

### Added
Expand Down
371 changes: 294 additions & 77 deletions packages/coding-agent/src/modes/components/agent-hub.ts

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions packages/coding-agent/src/task/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1511,6 +1511,11 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
popLoopPhase();
}
}
if (event.type === "thinking_level_changed") {
progress.thinkingLevel = event.thinkingLevel;
scheduleProgress(true);
return;
}
if (event.type === "retry_fallback_applied") {
progress.resolvedModel = event.to;
scheduleProgress(true);
Expand Down Expand Up @@ -2367,6 +2372,8 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
const effectiveThinkingLevel = explicitThinkingLevel
? resolvedThinkingLevel
: (thinkingLevel ?? resolvedThinkingLevel);
progress.lspEnabled = lspEnabled;
progress.thinkingLevel = effectiveThinkingLevel === "auto" ? undefined : effectiveThinkingLevel;
resolvedAt = performance.now();

const effectiveCwd = worktree ?? cwd;
Expand Down Expand Up @@ -2502,6 +2509,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes

monitor.setActiveSession(session);
installRegistryStatusSync(session);
progress.thinkingLevel = session.thinkingLevel;
progress.advisorActive = session.isAdvisorActive?.();
monitor.scheduleProgress(true);
if (sessionFile !== null && worktree === undefined) {
// Lifecycle reviver: park closed the JSONL writer, so reopening takes
// the single-writer lock cleanly and restores the full message history
Expand Down
7 changes: 7 additions & 0 deletions packages/coding-agent/src/task/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
import type { Usage } from "@oh-my-pi/pi-ai";
import { $env } from "@oh-my-pi/pi-utils";
import { type BaseType, type } from "arktype";
Expand Down Expand Up @@ -361,6 +362,12 @@ export interface AgentProgress {
modelOverride?: string | string[];
/** Resolved model display string in the form `<provider>/<id>`, optionally suffixed with `:<thinkingLevel>` when the level was set explicitly. Undefined when the model could not be resolved. */
resolvedModel?: string;
/** Current reasoning level, when known from the live session. */
thinkingLevel?: ThinkingLevel;
/** Whether the session exposes the LSP tool. Undefined for older or cold-restored snapshots. */
lspEnabled?: boolean;
/** Whether an advisor is active for the session. Undefined for older or cold-restored snapshots. */
advisorActive?: boolean;
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
extractedToolData?: Record<string, unknown[]>;
/**
Expand Down
91 changes: 91 additions & 0 deletions packages/coding-agent/test/agent-hub-activate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { SelectorController } from "@oh-my-pi/pi-coding-agent/modes/controllers/
import { SessionObserverRegistry } from "@oh-my-pi/pi-coding-agent/modes/session-observer-registry";
import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
import type { InteractiveModeContext } from "@oh-my-pi/pi-coding-agent/modes/types";
import type { AgentLifecycleManager } from "@oh-my-pi/pi-coding-agent/registry/agent-lifecycle";
import { AgentRegistry } from "@oh-my-pi/pi-coding-agent/registry/agent-registry";
import type { AgentSession } from "@oh-my-pi/pi-coding-agent/session/agent-session";
import { TempDir } from "@oh-my-pi/pi-utils";
Expand Down Expand Up @@ -89,6 +90,95 @@ describe("Agent hub Enter activation", () => {
hub.dispose();
});

it("t opens the active transcript without changing focus", () => {
const focusedIds: string[] = [];
const agents = new AgentRegistry();
agents.register({
id: AGENT_ID,
displayName: AGENT_ID,
kind: "sub",
session: { subscribe: () => () => {} } as unknown as AgentSession,
status: "running",
});
let overlayCalls = 0;
const hub = new AgentHubOverlayComponent({
observers: new SessionObserverRegistry(),
hubKeys: [],
onDone: () => {},
requestRender: () => {},
registry: agents,
irc: new IrcBus(agents),
focusAgent: async id => {
focusedIds.push(id);
},
ui: {
showOverlay: () => {
overlayCalls++;
return { hide: () => {} };
},
setFocus: () => {},
requestRender: () => {},
requestComponentRender: () => {},
} as never,
});

hub.handleInput("t");
expect(overlayCalls).toBe(1);
expect(focusedIds).toEqual([]);
hub.dispose();
});

it("archive Enter opens a transcript and r is the only revive action", async () => {
const agents = new AgentRegistry();
agents.register({
id: AGENT_ID,
displayName: AGENT_ID,
kind: "sub",
session: null,
sessionFile: "/tmp/worker.jsonl",
status: "parked",
});
const focusedIds: string[] = [];
const revivedIds: string[] = [];
let overlayCalls = 0;
const hub = new AgentHubOverlayComponent({
observers: new SessionObserverRegistry(),
hubKeys: [],
onDone: () => {},
requestRender: () => {},
registry: agents,
irc: new IrcBus(agents),
focusAgent: async id => {
focusedIds.push(id);
},
lifecycle: {
ensureLive: async (id: string) => {
revivedIds.push(id);
return {} as AgentSession;
},
} as unknown as AgentLifecycleManager,
ui: {
showOverlay: () => {
overlayCalls++;
return { hide: () => {} };
},
setFocus: () => {},
requestRender: () => {},
requestComponentRender: () => {},
} as never,
});

hub.handleInput("\t");
hub.handleInput("\r");
expect(overlayCalls).toBe(1);
expect(focusedIds).toEqual([]);
expect(revivedIds).toEqual([]);
hub.handleInput("r");
await Promise.resolve();
expect(revivedIds).toEqual([AGENT_ID]);
hub.dispose();
});

it("lists persisted subagent session files after restart", async () => {
using tempDir = TempDir.createSync("@omp-agent-hub-persisted-");
const sessionFile = path.join(tempDir.path(), "main.jsonl");
Expand All @@ -108,6 +198,7 @@ describe("Agent hub Enter activation", () => {
});
await hub.persistedSubagentsReady;

hub.handleInput("\t");
const rendered = Bun.stripANSI(hub.render(120).join("\n"));
expect(rendered).toContain("Worker");
expect(rendered).toContain("parked");
Expand Down
Loading
Loading