Skip to content
Open
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
Expand Up @@ -144,17 +144,27 @@ export function ChannelTimeline({
// visible; no prose-based inference that a waiting run is safe to ignore.
const routineRuns = items.filter((item): item is Extract<WorkspaceTimelineItem, { kind: "run" }> =>
item.kind === "run" && ["queued", "running", "completed"].includes(item.run.status));
const routineIds = new Set(routineRuns.map(item => item.id));
const primaryItems = items.filter(item => item.kind !== "proposal" && !routineIds.has(item.id));
const scheduleItems = items.filter((item): item is Extract<WorkspaceTimelineItem, { kind: "schedule" }> => item.kind === "schedule");
const backgroundIds = new Set([...routineRuns, ...scheduleItems].map(item => item.id));
const primaryItems = items.filter(item => item.kind !== "proposal" && !backgroundIds.has(item.id));
const pausedScheduleCount = scheduleItems.filter(item => item.schedule.status === "paused").length;
const enabledScheduleCount = scheduleItems.length - pausedScheduleCount;
const workingCount = routineRuns.filter(item => item.run.status === "running" && Boolean(item.run.sessionId) && Boolean(item.run.canInterrupt)).length;
const queuedCount = routineRuns.filter(item => item.run.status === "queued").length;
const completedCount = routineRuns.filter(item => item.run.status === "completed").length;
const progressCount = routineRuns.length - workingCount - queuedCount - completedCount;
const activitySummary = locale === "zh-CN"
? [workingCount && `${workingCount} 个执行中`, queuedCount && `${queuedCount} 个排队中`, completedCount && `${completedCount} 次执行已结束`, progressCount && `${progressCount} 项进展更新`].filter(Boolean).join(" · ")
: [workingCount && `${workingCount} running`, queuedCount && `${queuedCount} queued`, completedCount && `${completedCount} runs finished`, progressCount && `${progressCount} progress updates`].filter(Boolean).join(" · ");
const activitySummary = ([
enabledScheduleCount ? t("timeline.backgroundSchedules", { count: enabledScheduleCount }) : null,
pausedScheduleCount ? t("timeline.backgroundPaused", { count: pausedScheduleCount }) : null,
] as Array<string | number | null>).concat(locale === "zh-CN"
? [workingCount && `${workingCount} 个执行中`, queuedCount && `${queuedCount} 个排队中`, completedCount && `${completedCount} 次执行已结束`, progressCount && `${progressCount} 项进展更新`]
: [workingCount && `${workingCount} running`, queuedCount && `${queuedCount} queued`, completedCount && `${completedCount} runs finished`, progressCount && `${progressCount} progress updates`]).filter(Boolean).join(" · ");
const activeProposalItems = items.filter((item): item is Extract<WorkspaceTimelineItem, { kind: "proposal" }> =>
item.kind === "proposal" && item.proposal.status !== "gated");
// Only drafts awaiting the owner fold behind the newest one; applying, applied and failed results stay visible.
const readyProposalItems = activeProposalItems.filter(item => item.proposal.status === "ready");
const foldedProposalIds = new Set(readyProposalItems.slice(0, -1).map(item => item.id));
const visibleProposalItems = activeProposalItems.filter(item => !foldedProposalIds.has(item.id));

function renderItem(item: WorkspaceTimelineItem) {
if (item.kind === "attention") {
Expand All @@ -172,10 +182,10 @@ export function ChannelTimeline({
if (item.kind === "proposal") {
const appliedTeamPlan = item.proposal.actionKind === "team.plan" && item.proposal.status === "applied";
return (
<Fragment key={item.id}><button className={`personal-proposal-row is-${item.proposal.status}`} onClick={() => onSelect({ item: item.proposal, kind: "proposal" })} type="button">
<Fragment key={item.id}><button className={`personal-proposal-row is-${item.proposal.status}`} data-action-kind={item.proposal.actionKind} onClick={() => onSelect({ item: item.proposal, kind: "proposal" })} type="button">
<span><Sparkles size={17} /></span>
<span><small>{appliedTeamPlan ? (locale === "zh-CN" ? "团队分配 · 已记录" : "Team assignment · Recorded")
: `${item.proposal.actionKind} · ${item.proposal.status}`}</small><strong>{item.proposal.title}</strong>{item.proposal.impact ? <p>{item.proposal.impact}</p> : null}</span>
: `${t(`proposal.kind.${item.proposal.actionKind}`)} · ${t(`proposal.status.${item.proposal.status}`)}`}</small><strong>{item.proposal.title}</strong>{item.proposal.impact ? <p>{item.proposal.impact}</p> : null}</span>
<b>{item.proposal.status === "gated" && item.proposal.actionKind !== "operation.execute" ? t("timeline.review") : item.proposal.primaryLabel ?? t("timeline.reviewAndConfirm")}</b>
</button>{showManagerTeamResults && onOpenGoalEvidence && appliedTeamPlan
&& item.proposal.goalId && item.proposal.teamPlanTodoIds?.length
Expand Down Expand Up @@ -207,9 +217,9 @@ export function ChannelTimeline({
<>
<p aria-atomic="true" aria-live="polite" className="personal-live-region" role="status">{liveAnnouncement}</p>
<div className="personal-channel-timeline">
{routineRuns.length ? <details className="personal-activity-summary">
<summary><Activity size={16} aria-hidden="true"/><strong>{locale === "zh-CN" ? "执行动态" : "Execution activity"}</strong><span>{activitySummary}</span></summary>
<div>{routineRuns.map(renderItem)}</div>
{backgroundIds.size ? <details className="personal-activity-summary">
<summary><Activity size={16} aria-hidden="true"/><strong>{t("timeline.background")}</strong><span>{activitySummary}</span></summary>
<div>{scheduleItems.map(renderItem)}{routineRuns.map(renderItem)}</div>
</details> : null}
{primaryItems.map(renderItem)}
{gatedItems.length ? (
Expand All @@ -218,7 +228,13 @@ export function ChannelTimeline({
<div>{gatedItems.map(renderItem)}</div>
</details>
) : null}
{activeProposalItems.map(renderItem)}
{foldedProposalIds.size ? (
<details className="personal-proposal-backlog">
<summary><Sparkles size={16} aria-hidden="true" /><strong>{t("timeline.olderDrafts", { count: foldedProposalIds.size })}</strong></summary>
<div>{readyProposalItems.filter(item => foldedProposalIds.has(item.id)).map(renderItem)}</div>
</details>
) : null}
{visibleProposalItems.map(renderItem)}
</div>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,32 @@ const en = {
"activity.claimedAt": "claimed {time}",
"activity.viaHost": "{host} host",
"activity.quiet": "no new activity for {minutes} min",
"proposal.kind.goal.create": "New Goal",
"proposal.kind.goal.update": "Goal update",
"proposal.kind.goal.lifecycle": "Goal lifecycle",
"proposal.kind.todo.create": "New Task",
"proposal.kind.todo.update": "Task update",
"proposal.kind.agent.bind": "Agent binding",
"proposal.kind.heartbeat.bind": "Heartbeat",
"proposal.kind.monitor.create": "New scheduled check",
"proposal.kind.monitor.update": "Scheduled check change",
"proposal.kind.gate.resolve": "Confirmation",
"proposal.kind.run.correct": "Run correction",
"proposal.kind.operation.execute": "Operation",
"proposal.kind.team.plan": "Team assignment",
"proposal.status.draft": "Draft",
"proposal.status.ready": "Awaiting you",
"proposal.status.applying": "Applying",
"proposal.status.applied": "Applied",
"proposal.status.gated": "Needs authorization",
"proposal.status.stale": "Out of date",
"proposal.status.error": "Failed",
"proposal.status.rejected": "Rejected",
"proposal.status.deferred": "Deferred",
"timeline.background": "Background work",
"timeline.backgroundSchedules": "{count} scheduled enabled",
"timeline.backgroundPaused": "{count} paused",
"timeline.olderDrafts": "{count} more drafts awaiting you",
"brief.title": "Today's brief",
"brief.needs": "Needs you",
"brief.needsEmpty": "Nothing is waiting on you",
Expand Down Expand Up @@ -2178,6 +2204,32 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"activity.claimedAt": "领取于{time}",
"activity.viaHost": "{host} 宿主",
"activity.quiet": "已 {minutes} 分钟无新动静",
"proposal.kind.goal.create": "新 Goal",
"proposal.kind.goal.update": "更新 Goal",
"proposal.kind.goal.lifecycle": "Goal 启停",
"proposal.kind.todo.create": "新任务",
"proposal.kind.todo.update": "更新任务",
"proposal.kind.agent.bind": "绑定 Agent",
"proposal.kind.heartbeat.bind": "设置 Heartbeat",
"proposal.kind.monitor.create": "新定时检查",
"proposal.kind.monitor.update": "调整定时检查",
"proposal.kind.gate.resolve": "处理确认项",
"proposal.kind.run.correct": "纠偏执行",
"proposal.kind.operation.execute": "执行操作",
"proposal.kind.team.plan": "团队分配",
"proposal.status.draft": "草稿",
"proposal.status.ready": "待你确认",
"proposal.status.applying": "正在应用",
"proposal.status.applied": "已应用",
"proposal.status.gated": "等待授权",
"proposal.status.stale": "已过期",
"proposal.status.error": "出错",
"proposal.status.rejected": "已拒绝",
"proposal.status.deferred": "已延后",
"timeline.background": "后台工作",
"timeline.backgroundSchedules": "{count} 个定时任务已启用",
"timeline.backgroundPaused": "{count} 个已暂停",
"timeline.olderDrafts": "另有 {count} 个待确认提议",
"brief.title": "今日简报",
"brief.needs": "等你处理",
"brief.needsEmpty": "没有等你处理的事",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/personal-workspace-browser/steward-journey.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export const stewardJourneyScenario = {
if (!turn) await page.waitForTimeout(50);
}
check(Boolean(turn), "the steward prompt reaches the Goal conversation as an accepted Turn");
const row = page.locator(".personal-proposal-row.is-ready", { hasText: "team.plan" })
const row = page.locator('.personal-proposal-row.is-ready[data-action-kind="team.plan"]')
.filter({ hasText: GOAL_ID });
await row.waitFor({ state: "visible", timeout: 15_000 });
await row.click();
Expand Down
4 changes: 2 additions & 2 deletions examples/personal-workspace-browser/team-plan.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export const teamPlanScenario = {
+ ` body=${(await page.locator("body").innerText()).slice(0, 1500)}`,
);
}
check((await row.innerText()).includes("team.plan"), "the proposal row names the team.plan action kind");
check(await row.getAttribute("data-action-kind") === "team.plan" && (await row.innerText()).includes("团队分配"), "the proposal row is a team.plan action labelled for the owner");

await row.click();
const drawer = page.locator('.personal-context-drawer[data-context-kind="proposal"]');
Expand Down Expand Up @@ -238,7 +238,7 @@ export const teamPlanScenario = {
);
}
check(
(await managerCard.innerText()).includes("team.plan"),
await managerCard.getAttribute("data-action-kind") === "team.plan",
"the manager conversation offers the team plan card it produced",
);
await page.screenshot({
Expand Down
4 changes: 4 additions & 0 deletions examples/personal-workspace-browser/typed-actions.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,9 @@ export const typedActionsScenario = {
await page.getByRole("button", { name: "View updated Goal", exact: true }).click();
await page.getByRole("navigation", { name: "Goal view" }).getByRole("button", { name: /^(Chat|对话)$/, exact: true }).click();
const englishHeartbeatSchedule = page.locator(".personal-schedule-row", { hasText: "Goal Heartbeat" }).first();
await page.locator(".personal-activity-summary > summary", { hasText: "Background work" }).waitFor({ state: "visible" });
if (await englishHeartbeatSchedule.isVisible()) throw new Error("Schedules must fold under background work instead of crowding the conversation");
await page.locator(".personal-activity-summary > summary").click();
await englishHeartbeatSchedule.waitFor({ state: "visible" });
const englishHeartbeatScheduleText = await englishHeartbeatSchedule.innerText();
if (!englishHeartbeatScheduleText.includes("1d")) throw new Error("Applied English Heartbeat lost cadence: " + englishHeartbeatScheduleText);
Expand Down Expand Up @@ -1793,6 +1796,7 @@ export const typedActionsScenario = {

await goalNavigation.getByRole("button", { name: /^(Chat|对话)$/ }).click();
const schedule = page.locator(".personal-schedule-row").first();
if (!await schedule.isVisible()) await page.locator(".personal-activity-summary > summary").click();
for (const [label, operation] of [["立即运行", "run_now"], ["暂停", "pause"], ["改为每 2 小时", "edit"], ["停止定时检查", "stop"]]) {
await schedule.click();
await page.getByText("定时检查", { exact: true }).last().waitFor({ state: "visible" });
Expand Down
Loading