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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CSSProperties } from "react";
import { goalHostDeliveryWindows, goalHostSurfaces, goalIdentity, hostSurfaceLabel, presentGoalActivity, type GoalActivity, type WorkspaceGoalExecution } from "./goal-activity";
import { goalHostSurfaces, goalIdentity, goalObservationGaps, hostSurfaceLabel, presentGoalActivity, type GoalActivity, type WorkspaceGoalExecution } from "./goal-activity";
import { ContextHelp } from "./context-help";
import { useWorkspaceI18n, type WorkspaceMessageKey } from "./i18n";
import type { WorkspaceGoal } from "./personal-workspace-model";

Expand Down Expand Up @@ -44,35 +45,58 @@ export function useExecutionDetail(execution: WorkspaceGoalExecution | undefined

export function useGoalActivity(goal: GoalActivitySubject): GoalActivity & {
text: string;
windowBadge: { state: "stale" | "missing"; labelKey: "activity.hostWindowStaleShort" | "activity.hostWindowMissingShort" } | null;
windowBadge: { state: "stale" | "missing"; text: string } | null;
} {
const { t } = useWorkspaceI18n();
const activity = presentGoalActivity(goal);
const detail = useExecutionDetail(goal.execution);
const hostWindows = goalHostDeliveryWindows(goal);
const gaps = goalObservationGaps(goal);
const parts = [t(activity.labelKey as WorkspaceMessageKey)];
if (activity.alsoKey) parts.push(t(activity.alsoKey as WorkspaceMessageKey, { host: hostNames(goalHostSurfaces(goal)) }));
if (activity.alsoKey && !(gaps.unavailable && activity.alsoKey === "activity.executionUnknown")) parts.push(t(activity.alsoKey as WorkspaceMessageKey, { host: hostNames(goalHostSurfaces(goal)) }));
if (activity.labelKey === "activity.running" || activity.labelKey === "activity.hostClaimed") parts.push(...detail);
for (const hostWindow of hostWindows) {
const host = hostSurfaceLabel(hostWindow.hostSurface);
const agent = hostWindow.agentId || "Agent";
if (hostWindow.state === "stale") {
parts.push(t("activity.hostWindowStale", { host, agent, hours: Math.round((hostWindow.ageHours ?? 0) * 10) / 10 }));
} else if (hostWindow.state === "missing") {
parts.push(t("activity.hostWindowMissing", { host, agent }));
} else {
parts.push(t("activity.hostWindowFresh", { host, agent }));
}
}
const missedWindow = hostWindows.find((window) => window.state === "stale" || window.state === "missing");
const windowBadge = missedWindow?.state === "stale"
? { state: "stale" as const, labelKey: "activity.hostWindowStaleShort" as const }
: missedWindow?.state === "missing"
? { state: "missing" as const, labelKey: "activity.hostWindowMissingShort" as const }
: null;
const windowBadge = goal.activationState === "stopped" || (activity.labelKey === "activity.observationUnavailable" && gaps.unavailable)
|| (!gaps.missing && !gaps.stale && !gaps.incomplete) ? null : {
state: gaps.stale ? "stale" as const : "missing" as const,
text: gaps.unavailable ? t("activity.observationUnavailable")
: gaps.missing || gaps.stale ? t("activity.observationGaps", { count: gaps.missing + gaps.stale })
: t("activity.observationIncomplete"),
};
return { ...activity, text: parts.join(" · "), windowBadge };
}

const observationReasons: Readonly<Record<string, WorkspaceMessageKey>> = {
unsupported_host: "activity.reason.unsupportedHost",
store_unavailable: "activity.reason.storeUnavailable",
thread_not_found: "activity.reason.threadNotFound",
record_unrecognized: "activity.reason.recordUnrecognized",
no_turn_marker: "activity.reason.noTurnMarker",
unparseable_observation: "activity.reason.unparseableObservation",
};

/** A sibling of the Goal navigation button so help never selects a Goal. */
export function GoalActivityHelp({ goal }: { goal: GoalActivitySubject }) {
const { locale, t } = useWorkspaceI18n();
const threads = goal.hostThreadActivity?.threads ?? [];
if (!goal.hostThreadActivity || goal.activationState === "stopped" || goal.loadState) return null;
return <ContextHelp label={t("activity.observationDetails", { goal: goal.title })}>
{goal.hostThreadActivity?.completeness === "incomplete" ? <p>{t("activity.observationIncomplete")}</p> : null}
<ul className="personal-goal-observation-list">{threads.map((thread, index) => {
const reason = thread.reason || thread.deliveryWindow?.reason;
const time = thread.lastEventAt ? relativeTime(thread.lastEventAt, locale) : null;
const state = thread.state === "turn_open" ? "activity.observedOpen"
: thread.state === "idle" ? "activity.observedEnded"
: thread.state === "archived" ? "activity.observedArchived" : "activity.observationUnavailable";
return <li key={`${thread.agentId}:${thread.hostSurface}:${index}`}>
<strong>{thread.agentId || "Agent"} · {hostSurfaceLabel(thread.hostSurface)}</strong>
<p>{t(state)}{time ? ` · ${time}` : ""}</p>
{thread.deliveryWindow?.state === "stale" ? <p>{t("activity.hostWindowStale", {hours: Math.round((thread.deliveryWindow.ageHours ?? 0) * 10) / 10})}</p> : null}
{reason && observationReasons[reason] ? <p>{t(observationReasons[reason])}</p>
: thread.state === "unknown" ? <p>{t("activity.reason.recordUnrecognized")}</p> : null}
</li>;
})}</ul>
</ContextHelp>;
}

export function GoalIdentityMark({ goal, size = "md" }: { goal: GoalActivitySubject; size?: "md" | "lg" }) {
const activity = presentGoalActivity(goal);
const identity = goalIdentity(goal.goalId, goal.title);
Expand All @@ -88,12 +112,11 @@ export function GoalIdentityMark({ goal, size = "md" }: { goal: GoalActivitySubj
}

export function GoalActivityChip({ goal }: { goal: GoalActivitySubject }) {
const { t } = useWorkspaceI18n();
const activity = useGoalActivity(goal);
return (
<span className={`personal-goal-activity-chip is-${activity.tone}${activity.live ? " is-live" : ""}`} title={activity.text}>
<span aria-hidden="true" className="personal-goal-activity-dot" />
{activity.windowBadge ? <span className={`personal-goal-window-badge is-${activity.windowBadge.state}`}>{t(activity.windowBadge.labelKey)}</span> : null}
{activity.windowBadge ? <span className={`personal-goal-window-badge is-${activity.windowBadge.state}`}>{activity.windowBadge.text}</span> : null}
<span>{activity.text}</span>
</span>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import assert from "node:assert/strict";
import { goalExecution, goalHostDeliveryWindows, goalWorkKind, presentGoalActivity, taskSessionStatus } from "../../../node_modules/.cache/loopx-goal-activity/goal-activity.js";
import { goalExecution, goalObservationGaps, goalWorkKind, presentGoalActivity, taskSessionStatus } from "../../../node_modules/.cache/loopx-goal-activity/goal-activity.js";

// One Goal's session facts, mixed by mode. `updated_at` stands for the time the
// owner last recorded anything for that session.
Expand All @@ -19,19 +19,9 @@ const recent = minutesAgo(2);
const silent = minutesAgo(30);
const old = minutesAgo(45);

assert.deepEqual(goalHostDeliveryWindows({ hostThreadActivity: { threads: [
{ agentId: "codex", hostSurface: "codex-app", state: "idle", lastEventAt: recent, deliveryWindow: { state: "stale", ageHours: 3.5 } },
{ agentId: "cursor-agent", hostSurface: "cursor", state: "idle", lastEventAt: recent, deliveryWindow: { state: "fresh", ageHours: 0.2 } },
] } }), [
{ agentId: "codex", hostSurface: "codex-app", state: "stale", ageHours: 3.5 },
{ agentId: "cursor-agent", hostSurface: "cursor", state: "fresh", ageHours: 0.2 },
]);
assert.deepEqual(goalHostDeliveryWindows({ hostThreadActivity: { threads: [
{ agentId: "codex", hostSurface: "codex-app", state: "unknown", lastEventAt: null, deliveryWindow: { state: "missing", reason: "no_observation" } },
] } }), [{ agentId: "codex", hostSurface: "codex-app", state: "missing", ageHours: null }]);
assert.deepEqual(goalHostDeliveryWindows({ hostThreadActivity: { threads: [
assert.deepEqual(goalObservationGaps({ hostThreadActivity: {completeness: "complete", threads: [
{ agentId: "codex", hostSurface: "codex-app", state: "idle", lastEventAt: recent, deliveryWindow: { state: "unknown", reason: "no_expectation" } },
] } }), [], "an unavailable cadence expectation is not a missed window");
] } }), {missing: 0, stale: 0, unavailable: false, incomplete: false}, "an unavailable cadence expectation is not a missed window");

function executionOf(sessions, observation) {
const execution = goalExecution(sessions, "goal-a", observation, now);
Expand Down Expand Up @@ -103,6 +93,37 @@ assert.equal(freshHost.quiet, false);
assert.equal(presentGoalActivity({ activationState: "active", execution: freshHost, state: "推进" }).live, true);
assert.equal(goalExecution(undefined, "goal-a", hostObservation(recent), now).kind, "running");
assert.equal(goalExecution(null, "goal-a", hostObservation(minutesAgo(8 * 60)), now).kind, "unknown");
assert.equal(goalExecution([], "goal-a", hostObservation(minutesAgo(8 * 60)), now).kind, "unknown", "An old open marker proves neither current execution nor an ended turn");
const unreadable = { hostSurface: "codex-app", state: "unknown", reason: "no_turn_marker", lastEventAt: null,
deliveryWindow: { state: "missing", reason: "no_observation" } };
const mixedHosts = { completeness: "complete", threads: [...hostObservation(recent).threads, unreadable] };
assert.equal(executionOf([], mixedHosts).lastActivityAt, recent, "An unreadable lane cannot replace another lane's clock");
assert.equal(goalObservationGaps({hostThreadActivity: mixedHosts}).unavailable, false);
assert.equal(goalObservationGaps({hostThreadActivity: mixedHosts}).missing, 1);
for (const reason of ["unsupported_host", "thread_not_found", "store_unavailable", "record_unrecognized", "no_turn_marker"]) {
const observation = {completeness: "complete", threads: [{...unreadable, reason}]};
for (const sessions of [undefined, null, []]) {
assert.equal(goalExecution(sessions, "goal-a", observation, now).kind, "unknown", `${reason}: absent host evidence cannot establish idleness`);
}
assert.equal(goalObservationGaps({hostThreadActivity: observation}).unavailable, true);
assert.equal(executionOf([session("managed", "managed_runtime", recent)], observation).lastActivityAt, recent,
"Managed execution also survives a missing host observation");
}
const endedAndMissing = {completeness: "complete", threads: [
{hostSurface: "codex-app", state: "idle", lastEventAt: recent, deliveryWindow: {state: "fresh"}}, unreadable,
]};
assert.equal(goalExecution([], "goal-a", endedAndMissing, now).kind, "unknown", "Recent completed activity is not execution or proof of all-host idleness");
assert.deepEqual(goalObservationGaps({hostThreadActivity: endedAndMissing}), {missing: 1, stale: 0, unavailable: false, incomplete: false});
assert.equal(goalExecution([], "goal-a", {completeness: "incomplete", threads: []}, now).kind, "unknown");
assert.equal(goalExecution([], "goal-a", {completeness: "complete", threads: [{hostSurface: "codex-app", state: "archived", lastEventAt: null}]}, now).kind, "idle");
assert.equal(goalObservationGaps({hostThreadActivity: {completeness: "complete", threads: [
{hostSurface: "codex-app", state: "idle", lastEventAt: old, deliveryWindow: {state: "stale"}}, unreadable,
]}}).stale, 1, "An actual cadence miss stays visible beside missing coverage");
assert.deepEqual(goalObservationGaps({hostThreadActivity: {completeness: "complete", threads: [
{hostSurface: "codex-app", state: "turn_open", lastEventAt: "unreadable", deliveryWindow: {state: "stale"}},
]}}), {missing: 1, stale: 0, incomplete: false, unavailable: true}, "One unreadable lane is counted once even when its cadence payload is inconsistent");
assert.equal(presentGoalActivity({activationState: "active", state: "等待条件", execution: {kind: "unknown"}, hostThreadActivity: {completeness: "complete", threads: [unreadable]}}).labelKey,
"state.waiting", "A known wait condition survives missing execution coverage");
for (const completeness of ["incomplete", undefined]) {
const partialIdle = presentGoalActivity({
activationState: "active", state: "已安排", boundHostSurfaces: ["codex-app"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export type GoalHostThread = {
agentId?: string | null;
hostSurface: string;
state: "turn_open" | "idle" | "archived" | "unknown";
/** Host-owned read failure, distinct from the cadence window's reason. */
reason?: string | null;
lastEventAt: string | null;
deliveryWindow?: {
state: "fresh" | "stale" | "missing" | "unknown";
Expand Down Expand Up @@ -100,6 +102,10 @@ export function goalExecution(
const open = (sessions ?? []).filter((session) => session.goal_id === goalId && session.status !== "closed");
const active = open.filter((session) => Boolean(session.active_turn_id));
if (active.length === 0 && hostTurns.length === 0) {
// An idle managed-session list cannot establish that unreadable or omitted
// host lanes are idle. Keep their coverage gap independent of other lanes.
if (hostThreadActivity && (hostThreadActivity.completeness !== "complete"
|| hostThreadActivity.threads.some((thread) => thread.state !== "idle" && thread.state !== "archived"))) return { kind: "unknown" };
if (sessions === undefined) return undefined;
return sessions === null ? { kind: "unknown" } : { kind: "idle", hostSurfaces: hostSurfacesOf(open) };
}
Expand Down Expand Up @@ -149,18 +155,23 @@ export function goalHostSurfaces(goal: Pick<GoalActivityInput, "boundHostSurface
return Array.from(new Set([...(goal.boundHostSurfaces ?? []), ...attached]));
}

/** Cadence evidence shown separately from execution; unknown/no-expectation is not an alert. */
export function goalHostDeliveryWindows(goal: Pick<GoalActivityInput, "hostThreadActivity">) {
const windows = (goal.hostThreadActivity?.threads ?? [])
.filter((thread) => thread.deliveryWindow?.state === "stale" || thread.deliveryWindow?.state === "missing" || thread.deliveryWindow?.state === "fresh")
.map((thread) => ({
agentId: thread.agentId ?? null,
hostSurface: thread.hostSurface,
state: thread.deliveryWindow!.state as "fresh" | "stale" | "missing",
ageHours: thread.deliveryWindow?.ageHours ?? null,
}));
const rank = { stale: 0, missing: 1, fresh: 2 } as const;
return windows.sort((a, b) => rank[a.state] - rank[b.state]);
/** Local presentation counts, not a Goal execution verdict. A missing lane
* cannot erase another lane's observed execution or recent completed turn. */
export function goalObservationGaps(goal: Pick<GoalActivityInput, "hostThreadActivity">) {
const observation = goal.hostThreadActivity;
const threads = observation?.threads ?? [];
let missing = 0, stale = 0;
for (const thread of threads) {
if (thread.state === "unknown" || thread.deliveryWindow?.state === "missing"
|| (thread.state === "turn_open" && (!thread.lastEventAt || Number.isNaN(Date.parse(thread.lastEventAt))))
|| thread.deliveryWindow?.reason === "unparseable_observation") missing++;
else if (thread.deliveryWindow?.state === "stale") stale++;
}
return {
missing, stale,
incomplete: observation?.completeness === "incomplete",
unavailable: threads.length > 0 && missing === threads.length,
};
}

export type GoalActivityTone = "running" | "attention" | "danger" | "waiting" | "queued" | "quiet" | "stopped";
Expand Down Expand Up @@ -214,6 +225,9 @@ export function presentGoalActivity(goal: GoalActivityInput): GoalActivity {
if (work === "unrecognized") return { labelKey: "activity.modeUnrecognized", tone: "attention", live: false, alsoKey: null };
if (work === "executing") return { labelKey: "activity.running", tone: live ? "running" : "attention", live, alsoKey: modeAlsoKey };
if (goal.state === "等待条件") return { labelKey: "state.waiting", tone: "waiting", live: false, alsoKey: null };
if (goal.execution?.kind === "unknown" && goal.hostThreadActivity && goal.state !== "已完成") {
return { labelKey: "activity.observationUnavailable", tone: "queued", live: false, alsoKey: null };
}
if (goal.state === "已安排") {
const hostThreads = goal.hostThreadActivity?.threads ?? [];
const hostsIdle = goal.hostThreadActivity?.completeness === "complete"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DesktopUpdate } from "./desktop-update";
import { useGoalOrder } from "./use-goal-order";
import { goalHierarchy, visibleGoalTree } from "./goal-hierarchy";

import { GoalIdentityMark, useGoalActivity } from "./goal-activity-view";
import { GoalActivityHelp, GoalIdentityMark, useGoalActivity } from "./goal-activity-view";
import { useWorkspaceI18n } from "./i18n";
import type { WorkspaceGoal, WorkspaceGoalArchiveLoadState } from "./personal-workspace-model";
import { StatusSourceSwitcher, type StatusSourceControl } from "./status-source-switcher";
Expand All @@ -16,7 +16,7 @@ function GoalRowActivity({ goal, showLoad }: { goal: WorkspaceGoal; showLoad: bo
return (
<small className={`is-${activity.tone}`} title={activity.text}>
<span className="personal-goal-row-activity-copy">{activity.text}</span>
{activity.windowBadge ? <span className={`personal-goal-window-badge is-${activity.windowBadge.state}`}>{t(activity.windowBadge.labelKey)}</span> : null}
{activity.windowBadge ? <span className={`personal-goal-window-badge is-${activity.windowBadge.state}`}>{activity.windowBadge.text}</span> : null}
</small>
);
}
Expand Down Expand Up @@ -103,8 +103,10 @@ export function GoalSidebar({
{tree.parents.has(goal.goalId) ? <span className="personal-sr-only">{t("sidebar.childOf", {goal: tree.byId.get(tree.parents.get(goal.goalId)!)!.title})}</span> : null}
<GoalRowActivity goal={goal} showLoad={!stopped || selectedGoalId === goal.goalId} />
</span>
{tree.invalidParents.has(goal.goalId) && goal.goalId !== rootGoalId ? <AlertTriangle size={14} aria-label={t("sidebar.invalidParent")}><title>{t("sidebar.invalidParent")}</title></AlertTriangle> : <ChevronRight size={15} />}
{tree.invalidParents.has(goal.goalId) && goal.goalId !== rootGoalId ? <AlertTriangle size={14} aria-label={t("sidebar.invalidParent")}><title>{t("sidebar.invalidParent")}</title></AlertTriangle>
: !sorting && goal.hostThreadActivity && !stopped && !goal.loadState ? null : <ChevronRight size={15} />}
</button>
{!sorting ? <GoalActivityHelp key={`${statusSourceControl?.activeSource.statusUrl}:${goal.sourceAccess?.namespace}:${goal.goalId}`} goal={goal} /> : null}
{!stopped && sorting ? <div className="personal-goal-move-actions">
<button type="button" aria-label={t("sidebar.moveUp", { goal: goal.title })} disabled={siblingGroups.get(tree.parents.get(goal.goalId) ?? null)?.[0]?.goalId === goal.goalId} onClick={() => ordering.moveBy(goal.goalId, -1)}><ArrowUp aria-hidden="true" size={13} /></button>
<button type="button" aria-label={t("sidebar.moveDown", { goal: goal.title })} disabled={siblingGroups.get(tree.parents.get(goal.goalId) ?? null)?.at(-1)?.goalId === goal.goalId} onClick={() => ordering.moveBy(goal.goalId, 1)}><ArrowDown aria-hidden="true" size={13} /></button>
Expand Down
Loading
Loading