chore: merge upstream/main into feat/secret-friendly-names
This commit is contained in:
@@ -42,6 +42,8 @@ describe("SkillMessageComponent", () => {
|
||||
// New look: an icon-tagged "skill" header with the name and a single meta line.
|
||||
expect(text).toContain("skill");
|
||||
expect(text).toContain("atomic-commit");
|
||||
expect(text).toContain("skill atomic-commit");
|
||||
expect(text).not.toContain("skill atomic-commit");
|
||||
expect(text).toContain("88 lines");
|
||||
|
||||
// The card is drawn with an outline.
|
||||
|
||||
@@ -34,6 +34,7 @@ import { DynamicBorder } from "./dynamic-border";
|
||||
|
||||
/** Refresh cadence for the relative-time column */
|
||||
const AGE_TICK_MS = 5_000;
|
||||
const DATA_CHANGE_RENDER_COALESCE_MS = 100;
|
||||
/** Double-tap window for the table's left-left "close hub" gesture. */
|
||||
const LEFT_TAP_WINDOW_MS = 500;
|
||||
|
||||
@@ -198,6 +199,7 @@ export class AgentHubOverlayComponent extends Container {
|
||||
#hubKeys: KeyId[];
|
||||
#unsubscribers: Array<() => void> = [];
|
||||
#ageTimer: NodeJS.Timeout | undefined;
|
||||
#dataChangeTimer?: NodeJS.Timeout;
|
||||
#remote: AgentHubRemote | undefined;
|
||||
/** Resolves after persisted historical subagents have been registered and rows refreshed. */
|
||||
readonly persistedSubagentsReady: Promise<void>;
|
||||
@@ -251,8 +253,8 @@ export class AgentHubOverlayComponent extends Container {
|
||||
this.#expandKeys = deps.expandKeys ?? ["ctrl+o"];
|
||||
this.#focusAgent = deps.focusAgent;
|
||||
|
||||
this.#unsubscribers.push(this.#registry.onChange(() => this.#onDataChange()));
|
||||
this.#unsubscribers.push(this.#observers.onChange(() => this.#onDataChange()));
|
||||
this.#unsubscribers.push(this.#registry.onChange(() => this.#scheduleDataChange()));
|
||||
this.#unsubscribers.push(this.#observers.onChange(() => this.#scheduleDataChange()));
|
||||
this.#ageTimer = setInterval(() => this.#requestRender(), AGE_TICK_MS);
|
||||
this.#ageTimer.unref?.();
|
||||
|
||||
@@ -285,6 +287,10 @@ export class AgentHubOverlayComponent extends Container {
|
||||
clearInterval(this.#ageTimer);
|
||||
this.#ageTimer = undefined;
|
||||
}
|
||||
if (this.#dataChangeTimer) {
|
||||
clearTimeout(this.#dataChangeTimer);
|
||||
this.#dataChangeTimer = undefined;
|
||||
}
|
||||
this.#closeTranscriptOverlay();
|
||||
}
|
||||
|
||||
@@ -355,6 +361,15 @@ export class AgentHubOverlayComponent extends Container {
|
||||
// Live data plumbing
|
||||
// ========================================================================
|
||||
|
||||
#scheduleDataChange(): void {
|
||||
if (this.#dataChangeTimer) return;
|
||||
this.#dataChangeTimer = setTimeout(() => {
|
||||
this.#dataChangeTimer = undefined;
|
||||
this.#onDataChange();
|
||||
}, DATA_CHANGE_RENDER_COALESCE_MS);
|
||||
this.#dataChangeTimer.unref?.();
|
||||
}
|
||||
|
||||
#onDataChange(): void {
|
||||
this.#refreshRows();
|
||||
this.#requestRender();
|
||||
|
||||
@@ -50,7 +50,7 @@ export class SkillMessageComponent extends Container {
|
||||
|
||||
// Header: icon-tag + skill name, with the invocation args trailing dimmed.
|
||||
const tag = theme.fg("customMessageLabel", theme.bold(`${theme.icon.extensionSkill} skill`));
|
||||
let header = `${tag} ${theme.fg("customMessageText", theme.bold(name))}`;
|
||||
let header = `${tag} ${theme.fg("customMessageText", theme.bold(name))}`;
|
||||
if (args) {
|
||||
header += ` ${theme.fg("dim", args)}`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { emergencyTerminalRestore } from "@oh-my-pi/pi-tui";
|
||||
import { postmortem } from "@oh-my-pi/pi-utils";
|
||||
|
||||
/**
|
||||
* Interactive mode and embeddable RPC client exports for the coding agent.
|
||||
*
|
||||
@@ -11,7 +8,3 @@ import { postmortem } from "@oh-my-pi/pi-utils";
|
||||
export * from "./interactive-mode";
|
||||
export * from "./rpc/rpc-client";
|
||||
export * from "./rpc/rpc-types";
|
||||
|
||||
postmortem.register("terminal-restore", () => {
|
||||
emergencyTerminalRestore();
|
||||
});
|
||||
|
||||
@@ -161,8 +161,11 @@ import {
|
||||
} from "./loop-limit";
|
||||
import { OAuthManualInputManager } from "./oauth-manual-input";
|
||||
import { countRunningSubagentBadgeAgents, getRunningSubagentBadgeRegistry } from "./running-subagent-badge";
|
||||
import type { ObservableSession } from "./session-observer-registry";
|
||||
import { SessionObserverRegistry } from "./session-observer-registry";
|
||||
import {
|
||||
type ObservableSession,
|
||||
type SessionObserverChangeKind,
|
||||
SessionObserverRegistry,
|
||||
} from "./session-observer-registry";
|
||||
import { createSessionTeardown, type SessionTeardown } from "./session-teardown";
|
||||
import { runProviderSetupWizard } from "./setup-wizard/lazy";
|
||||
import { interruptHint } from "./shared";
|
||||
@@ -343,9 +346,12 @@ class AnchoredLiveContainer extends Container implements NativeScrollbackLiveReg
|
||||
* before it auto-clears, mirroring the todo HUD's auto-clear timer. */
|
||||
const MODEL_CYCLE_TRACK_CLEAR_MS = 4000;
|
||||
|
||||
const SUBAGENT_HUD_VISIBLE_LIMIT = 8;
|
||||
const SUBAGENT_OBSERVER_UI_COALESCE_MS = 100;
|
||||
|
||||
/**
|
||||
* Build the anchored subagent HUD block: a bold accent "Subagents" header plus
|
||||
* one tree row per running agent in the same `Id: description` shape the
|
||||
* a bounded set of running-agent rows in the same `Id: description` shape the
|
||||
* inline task rows use (muted task preview when no description was given).
|
||||
* Layout mirrors the Todos HUD exactly: unindented header, then
|
||||
* `renderTreeList` rows (dim connectors) shifted right by one space.
|
||||
@@ -361,9 +367,11 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n
|
||||
if (running.length === 0) return [];
|
||||
|
||||
const dot = theme.styledSymbol("status.done", "accent");
|
||||
const visible = running.slice(0, SUBAGENT_HUD_VISIBLE_LIMIT);
|
||||
const hiddenCount = running.length - visible.length;
|
||||
const rows = renderTreeList(
|
||||
{
|
||||
items: running,
|
||||
items: visible,
|
||||
expanded: true,
|
||||
renderItem: session => {
|
||||
const displayId = formatTaskId(session.id);
|
||||
@@ -385,6 +393,9 @@ export function renderSubagentHudLines(sessions: ObservableSession[], columns: n
|
||||
},
|
||||
theme,
|
||||
);
|
||||
if (hiddenCount > 0) {
|
||||
rows.push(theme.fg("dim", `… ${hiddenCount} more running — open Agent Hub for full list`));
|
||||
}
|
||||
return ["", theme.bold(theme.fg("accent", "Subagents")), ...rows.map(line => ` ${line}`)];
|
||||
}
|
||||
|
||||
@@ -588,6 +599,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
#observerRegistry: SessionObserverRegistry;
|
||||
#eventBus?: EventBus;
|
||||
#eventBusUnsubscribers: Array<() => void> = [];
|
||||
#observerUiSyncTimer?: NodeJS.Timeout;
|
||||
#observerUiSyncNeedsTodoReconcile = false;
|
||||
#agentRegistryUnsubscribe?: () => void;
|
||||
#agentRegistrySubscriptionTarget?: AgentRegistry;
|
||||
#mcpStatusOrder: string[] = [];
|
||||
@@ -917,17 +930,8 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
}
|
||||
this.#observerRegistry.setMainSession(this.sessionManager.getSessionFile() ?? undefined);
|
||||
this.syncRunningSubagentBadge();
|
||||
this.#observerRegistry.onChange(() => {
|
||||
this.syncRunningSubagentBadge();
|
||||
// Auto-checkmark todos whose matching subagent just succeeded, then
|
||||
// re-render so the running override (the static "live" glyph when a
|
||||
// subagent is doing the work for a still-pending todo) updates as
|
||||
// subagents start, finish, or fail.
|
||||
this.#reconcileTodosWithSubagents();
|
||||
this.#syncTodoAutoClearTimer();
|
||||
this.#renderTodoList();
|
||||
this.#renderSubagentList();
|
||||
this.ui.requestRender();
|
||||
this.#observerRegistry.onChange(kind => {
|
||||
this.#scheduleObserverUiSync(kind);
|
||||
});
|
||||
|
||||
// Load initial todos
|
||||
@@ -1521,19 +1525,18 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
}
|
||||
|
||||
/** Refresh the running-subagents status badge from the active local or collab registry. */
|
||||
syncRunningSubagentBadge(): void {
|
||||
syncRunningSubagentBadge(options: { requestRender?: boolean } = {}): void {
|
||||
const registry = getRunningSubagentBadgeRegistry(this.collabGuest);
|
||||
if (this.#agentRegistrySubscriptionTarget !== registry) {
|
||||
this.#agentRegistryUnsubscribe?.();
|
||||
this.#agentRegistrySubscriptionTarget = registry;
|
||||
this.#agentRegistryUnsubscribe = registry.onChange(() => {
|
||||
this.syncRunningSubagentBadge();
|
||||
this.ui.requestRender();
|
||||
});
|
||||
}
|
||||
const count = countRunningSubagentBadgeAgents(registry);
|
||||
this.statusLine.setSubagentCount(count);
|
||||
this.ui.requestRender();
|
||||
if (options.requestRender !== false) this.ui.requestRender();
|
||||
}
|
||||
|
||||
rebuildChatFromMessages(): void {
|
||||
@@ -1754,6 +1757,38 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
return active ?? nonEmpty[nonEmpty.length - 1];
|
||||
}
|
||||
|
||||
#scheduleObserverUiSync(kind: SessionObserverChangeKind): void {
|
||||
if (kind !== "progress") {
|
||||
this.#observerUiSyncNeedsTodoReconcile = true;
|
||||
}
|
||||
if (this.#observerUiSyncTimer) return;
|
||||
this.#observerUiSyncTimer = setTimeout(() => {
|
||||
this.#observerUiSyncTimer = undefined;
|
||||
this.#flushObserverUiSync();
|
||||
}, SUBAGENT_OBSERVER_UI_COALESCE_MS);
|
||||
this.#observerUiSyncTimer.unref?.();
|
||||
}
|
||||
|
||||
#flushObserverUiSync(): void {
|
||||
this.syncRunningSubagentBadge({ requestRender: false });
|
||||
if (this.#observerUiSyncNeedsTodoReconcile) {
|
||||
this.#observerUiSyncNeedsTodoReconcile = false;
|
||||
this.#reconcileTodosWithSubagents();
|
||||
}
|
||||
this.#syncTodoAutoClearTimer();
|
||||
this.#renderTodoList();
|
||||
this.#renderSubagentList();
|
||||
this.ui.requestRender();
|
||||
}
|
||||
|
||||
#cancelObserverUiSyncTimer(): void {
|
||||
if (this.#observerUiSyncTimer) {
|
||||
clearTimeout(this.#observerUiSyncTimer);
|
||||
this.#observerUiSyncTimer = undefined;
|
||||
}
|
||||
this.#observerUiSyncNeedsTodoReconcile = false;
|
||||
}
|
||||
|
||||
#renderTodoList(): void {
|
||||
this.todoContainer.clear();
|
||||
const phases = this.todoPhases.filter(phase => phase.tasks.length > 0);
|
||||
@@ -3306,6 +3341,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
||||
}
|
||||
this.#cleanupMicAnimation();
|
||||
this.#cancelTodoAutoClearTimer();
|
||||
this.#cancelObserverUiSyncTimer();
|
||||
this.#cancelGoalContinuation();
|
||||
if (this.#sttController) {
|
||||
this.#sttController.dispose();
|
||||
|
||||
@@ -24,6 +24,9 @@ export interface ObservableSession {
|
||||
progress?: AgentProgress;
|
||||
}
|
||||
|
||||
/** Coarse source of an observer change; callers use it to separate lifecycle work from high-frequency progress. */
|
||||
export type SessionObserverChangeKind = "main" | "reset" | "lifecycle" | "progress";
|
||||
|
||||
const STATUS_MAP: Record<string, ObservableSession["status"]> = {
|
||||
started: "active",
|
||||
completed: "completed",
|
||||
@@ -33,20 +36,20 @@ const STATUS_MAP: Record<string, ObservableSession["status"]> = {
|
||||
|
||||
export class SessionObserverRegistry {
|
||||
#sessions = new Map<string, ObservableSession>();
|
||||
#listeners = new Set<() => void>();
|
||||
#listeners = new Set<(kind: SessionObserverChangeKind) => void>();
|
||||
#eventBusUnsubscribers: Array<() => void> = [];
|
||||
#sortOrderById = new Map<string, number>();
|
||||
#parentSortOrderById = new Map<string, number>();
|
||||
#nextSortOrder = 0;
|
||||
|
||||
/** Add a change listener. Returns unsubscribe function. */
|
||||
onChange(cb: () => void): () => void {
|
||||
onChange(cb: (kind: SessionObserverChangeKind) => void): () => void {
|
||||
this.#listeners.add(cb);
|
||||
return () => this.#listeners.delete(cb);
|
||||
}
|
||||
|
||||
#notifyListeners(): void {
|
||||
for (const cb of this.#listeners) cb();
|
||||
#notifyListeners(kind: SessionObserverChangeKind): void {
|
||||
for (const cb of this.#listeners) cb(kind);
|
||||
}
|
||||
|
||||
#ensureSortOrder(id: string): number {
|
||||
@@ -85,7 +88,7 @@ export class SessionObserverRegistry {
|
||||
sessionFile: sessionFile ?? existing?.sessionFile,
|
||||
lastUpdate: Date.now(),
|
||||
});
|
||||
this.#notifyListeners();
|
||||
this.#notifyListeners("main");
|
||||
}
|
||||
|
||||
getSessions(): ObservableSession[] {
|
||||
@@ -121,7 +124,7 @@ export class SessionObserverRegistry {
|
||||
this.#sortOrderById.clear();
|
||||
this.#parentSortOrderById.clear();
|
||||
this.#nextSortOrder = 0;
|
||||
this.#notifyListeners();
|
||||
this.#notifyListeners("reset");
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
@@ -171,7 +174,7 @@ export class SessionObserverRegistry {
|
||||
lastUpdate: Date.now(),
|
||||
});
|
||||
}
|
||||
this.#notifyListeners();
|
||||
this.#notifyListeners("lifecycle");
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -208,7 +211,7 @@ export class SessionObserverRegistry {
|
||||
progress,
|
||||
});
|
||||
}
|
||||
this.#notifyListeners();
|
||||
this.#notifyListeners("progress");
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user