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
2 changes: 2 additions & 0 deletions apps/presentation/dashboard/src/data/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export const todoItemSchema = z.object({
todo_id: z.string().optional().nullable(),
role: z.string().optional().nullable(),
status: z.string().optional().nullable(),
resume_when: z.string().optional().nullable(),
priority: z.string().optional().nullable(),
title: z.string().optional().nullable(),
archive_state: z.string().optional().nullable(),
Expand All @@ -87,6 +88,7 @@ export const todoGroupSchema = z.object({
done_count: z.number().optional().default(0),
advancement_done_count: z.number().optional(),
items: z.array(todoItemSchema).optional().default([]),
deferred_items: z.array(todoItemSchema).optional(),
});

export const todoIndexItemSchema = todoItemSchema.extend({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@
<section className="personal-task-inspector-summary">
<div className="personal-task-inspector-status">
<span className={selection.item.done ? "is-done" : selection.item.status === "blocked" ? "is-blocked" : "is-open"}>
<i />{selection.item.done ? t("drawer.taskStatusCompleted") : selection.item.status === "blocked" ? t("drawer.taskStatusBlocked") : t("drawer.taskStatusOpen")}
<i />{selection.item.done ? t("drawer.taskStatusCompleted") : selection.item.status === "deferred" ? t("drawer.taskStatusDeferred") : selection.item.status === "blocked" ? t("drawer.taskStatusBlocked") : t("drawer.taskStatusOpen")}

Check warning on line 509 in apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaB1rkbXMwQY_bvu284r&open=AaB1rkbXMwQY_bvu284r&pullRequest=3990
</span>
{selection.item.priority ? <span>{selection.item.priority}</span> : null}
<span>{selection.item.taskClass === "advancement_task" ? t("drawer.taskAdvancement") : selection.item.taskClass ?? t("drawer.taskOrdinary")}</span>
Expand All @@ -518,10 +518,11 @@
<dl>
<div><dt>Goal</dt><dd>{selection.item.goalTitle}</dd></div>
<div><dt>{t("common.owner")}</dt><dd>{selection.item.ownerLabel ?? selection.item.claimedBy ?? t("drawer.notAssigned")}</dd></div>
<div><dt>{t("common.status")}</dt><dd>{selection.item.done ? t("drawer.taskStatusCompleted") : selection.item.status === "blocked" ? t("drawer.taskStatusBlocked") : t("drawer.taskStatusOpen")}</dd></div>
<div><dt>{t("common.status")}</dt><dd>{selection.item.done ? t("drawer.taskStatusCompleted") : selection.item.status === "deferred" ? t("drawer.taskStatusDeferred") : selection.item.status === "blocked" ? t("drawer.taskStatusBlocked") : t("drawer.taskStatusOpen")}</dd></div>

Check warning on line 521 in apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaB1rkbXMwQY_bvu284s&open=AaB1rkbXMwQY_bvu284s&pullRequest=3990
<div><dt>{t("drawer.priority")}</dt><dd>{selection.item.priority ?? t("drawer.notSet")}</dd></div>
<div><dt>{t("drawer.dependencies")}</dt><dd>{selection.item.dependencies?.join(" · ") || t("common.none")}</dd></div>
<div><dt>{t("drawer.nextTransition")}</dt><dd>{selection.item.nextTransition ?? (selection.item.done ? t("drawer.taskNextCompleted") : t("drawer.taskNextOpen"))}</dd></div>
{selection.item.status === "deferred" ? <div><dt>{t("drawer.resumeWhen")}</dt><dd>{selection.item.resumeWhen || t("drawer.notSet")}</dd></div> : null}
<div><dt>{t("drawer.nextTransition")}</dt><dd>{selection.item.nextTransition ?? (selection.item.done ? t("drawer.taskNextCompleted") : selection.item.status === "deferred" ? t("drawer.taskNextDeferred") : t("drawer.taskNextOpen"))}</dd></div>

Check warning on line 525 in apps/presentation/dashboard/src/features/personal-workspace/context-drawer.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaB1rkbXMwQY_bvu284t&open=AaB1rkbXMwQY_bvu284t&pullRequest=3990
</dl>
</section>
{!readOnly && !selection.item.done ? <div className="personal-task-inspector-actions" aria-label={t("drawer.taskActions")}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@
{todo.priority ? <span className={`personal-priority-badge is-${todo.priority.toLowerCase()}`}>{todo.priority}</span> : null}
{todo.status === "blocked" ? <span className="personal-priority-badge is-blocked">{t("tasks.blocked")}</span> : null}
{execution ? <span className="personal-task-session-status">{execution.status === "running" || execution.status === "queued" ? t("runs.running") : execution.status === "failed" ? t("tasks.sessionError") : t("common.waiting")}</span> : null}
{!execution ? <span className="personal-task-session-status">{t("tasks.waiting")}</span> : null}
{todo.status === "deferred" ? <span className="personal-task-session-status">{t("drawer.taskStatusDeferred")}</span> : !execution ? <span className="personal-task-session-status">{t("tasks.waiting")}</span> : null}

Check warning on line 254 in apps/presentation/dashboard/src/features/personal-workspace/goal-tasks-view.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=huangruiteng_loopx&issues=AaB1rkXYMwQY_bvu284q&open=AaB1rkXYMwQY_bvu284q&pullRequest=3990
{todo.claimedBy ?? goal.agentLabel ?? goal.agentId}
</small>
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ const en = {
"drawer.taskNextOpen": "Advance or update status",
"drawer.taskOrdinary": "Task",
"drawer.taskStatusBlocked": "Blocked",
"drawer.taskStatusDeferred": "Deferred",
"drawer.resumeWhen": "Resume condition",
"drawer.taskNextDeferred": "Wait for the resume condition, then reassess",
"drawer.taskStatusCompleted": "Completed",
"drawer.taskStatusOpen": "Ready",
"drawer.taskSuccessor": "Create follow-up Todo",
Expand Down Expand Up @@ -1124,6 +1127,9 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"drawer.taskNextOpen": "推进或更新状态",
"drawer.taskOrdinary": "普通任务",
"drawer.taskStatusBlocked": "受阻",
"drawer.taskStatusDeferred": "已延期",
"drawer.resumeWhen": "恢复条件",
"drawer.taskNextDeferred": "等待恢复条件满足后重新评估",
"drawer.taskStatusCompleted": "已完成",
"drawer.taskStatusOpen": "待执行",
"drawer.taskSuccessor": "创建后续 Todo",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type WorkspaceHomeLane =
| "stopped";

export type WorkspaceAgentTodo = {
resumeWhen?: string | null;
claimedBy?: string | null;
dependencies?: string[];
done: boolean;
Expand Down
26 changes: 21 additions & 5 deletions apps/presentation/dashboard/src/views/dashboard-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ type TodoExplorerItem = {
};

type PersonalAgentTodoItem = {
resumeWhen?: string | null;
claimedBy?: string | null;
done: boolean;
evidence?: string | null;
Expand Down Expand Up @@ -700,8 +701,10 @@ function personalTodoText(todo: TodoItem) {

function personalAgentTodoFromItem(todo: TodoItem, row: GoalDirectoryRow): PersonalAgentTodoItem {
return {
resumeWhen: todo.resume_when ?? null,
claimedBy: todo.claimed_by ?? null,
done: todo.done,
// Legacy summaries mark deferred entries checked; they are not completed work.
done: todo.status === "deferred" ? false : todo.done,
evidence: todo.evidence ? compactShareText(todo.evidence, 96) : null,
index: todo.index,
priority: todo.priority ?? null,
Expand All @@ -713,8 +716,21 @@ function personalAgentTodoFromItem(todo: TodoItem, row: GoalDirectoryRow): Perso
};
}

// Owner Workspace needs the queue, not the bounded public-share preview.
// Use the same items for cards and completion deduplication.
function personalAgentTodoItems(row: GoalDirectoryRow): TodoItem[] {
const queue = row.queueItem?.agent_todos;
const items = queue?.items ?? row.queueItem?.project_asset?.agent_todos?.items ?? [];
const merged = new Map(items.map((todo) => [todo.todo_id?.trim() || `${row.goal.id}:agent:${todo.index}`, todo]));
for (const todo of queue?.deferred_items ?? []) {
const key = todo.todo_id?.trim() || `${row.goal.id}:agent:${todo.index}`;
if (!merged.has(key)) merged.set(key, todo);
}
return [...merged.values()];
}

function personalAgentTodos(row: GoalDirectoryRow): PersonalAgentTodoItem[] {
return (getShareTodos(row, "agent")?.items ?? []).map((todo) => personalAgentTodoFromItem(todo, row));
return personalAgentTodoItems(row).map((todo) => personalAgentTodoFromItem(todo, row));
}

function personalSubagentDomainCandidates(
Expand Down Expand Up @@ -776,7 +792,7 @@ function mergePersonalAgentTodos(
/**
* Projected completion facts for a Goal. The status payload reports completed
* Todos as a count (project_asset.agent_todos.done) plus a bounded
* recent-completed lane; the items list itself only carries open Todos.
* recent-completed lane. Queue items may also include completed Todos.
*/
function personalAgentTodoFacts(row: GoalDirectoryRow): {
doneTodoCount: number;
Expand All @@ -785,13 +801,13 @@ function personalAgentTodoFacts(row: GoalDirectoryRow): {
} {
const assetTodos = row.queueItem?.project_asset?.agent_todos;
const queueTodos = row.queueItem?.agent_todos;
const items = assetTodos?.items?.length ? assetTodos.items : queueTodos?.items ?? [];
const items = personalAgentTodoItems(row);
const doneFromCount = assetTodos?.advancement_done_count
?? queueTodos?.advancement_done_count
?? assetTodos?.done
?? queueTodos?.done_count
?? null;
const doneFromItems = items.filter((todo) => todo.done).length;
const doneFromItems = items.filter((todo) => todo.done && todo.status !== "deferred").length;
const doneTodoCount = Math.max(doneFromCount ?? 0, doneFromItems);
const seenTodoIds = new Set(
items
Expand Down
49 changes: 49 additions & 0 deletions demo/workspace/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Workspace stories

Explore three connected projects in the real LoopX Personal Workspace: a 120-person community event, a home-energy buying guide, and a neighborhood website release. Each has four work roles, 18 delivery tasks across multiple phases, two owner decisions and two scheduled watches.

From a source checkout with Python 3.11+ and Node 22.6+:

```bash
python -m demo.workspace serve
```

Open the printed loopback URL. The command creates a disposable directory and serves the existing packaged frontend through the standard Chat backend. For a repeatable location:

```bash
python -m demo.workspace prepare --root /tmp/workspace-stories
python -m demo.workspace serve --root /tmp/workspace-stories --port 8791
```

Only a new empty directory or this demo's matching manifest is accepted. Prepare preserves an existing replay; choose a new directory to start again. Older manifest versions require a new directory.

## Projects and decisions

| Project | Work and constraints | Decisions |
| --- | --- | --- |
| Riverside Community Day | Three-venue comparison, $6000 limit, $5400 plan, $600 contingency, revised catering quote, access review, 18 volunteer shifts and two first-aid gaps | Venue reservation; invitation approval |
| Home Energy Buying Guide | 12 source cards, three household profiles, 27 tariff/efficiency combinations, conflicting assumptions, five-section editorial plan | Cost assumptions; publication approval |
| Riverside Neighborhood Website | Six pages, 18-route inventory, 24 accessibility criteria, navigation/form review findings, content permissions, rollback and handoff | Content freeze; deployment approval |

Each project has seven replayed completion checkpoints, five ready tasks, four blocked tasks and two deferred follow-ups. Dependency notes retain predecessor IDs; deferred tasks use real `todo_done` resume conditions. Two watch-only monitors have cadence and next-due metadata. No scheduler or live Agent is started by the demo.

Switch Board/List, filter by Agent, expand completed history, inspect the owner decisions and scheduled watches. `BRIEF.md`, `working-table.csv`, `calculations.json` and `DELIVERY-PLAN.md` preserve the planning inputs and dependencies. The energy sensitivity table and event contingency are calculated when preparing the workspace.

Replay one decision:

```bash
python -m demo.workspace advance --root /tmp/workspace-stories \
--story research-brief --decision assumptions
```

Refresh the UI afterward. Only that decision and its direct blocked successors are advanced. Other owner decisions and downstream blockers remain intact. The command does not purchase, publish, deploy or start an Agent.

## Data and isolation

These are authored scenario replays using real LoopX APIs and state transitions, not customer case studies or receipts of live Agent execution. Natural project titles keep the interface readable; the manifest and completion evidence retain provenance. Source-card inventories and website checklists are planning inputs, not claims of external research or executed website tests. Event money is a project budget, not model spending.

The demo does not import personal registries, session history or credentials, and does not sync into the global registry. The loopback server uses a separate HOME/CODEX_HOME, minimal environment and unavailable Agent/Lark binaries. Chat and Lark connection errors are intentional isolation and do not qualify live IM behavior. Stop with Ctrl-C.

This remains a source-checkout demo under `demo/`, outside the installed wheel and capability catalog. Screenshots and recordings belong in ignored `output/playwright/`. Keep real operating statistics separately timestamped with their counting scope.

Validation: `python -m pytest tests/test_workspace_story_demo.py -q` exercises real state, directory isolation, repeatable prepare, computed artifacts and decision-scoped transitions. The normal Workspace browser smoke covers the shared Board/List and completed-history behavior, including #3961.
1 change: 1 addition & 0 deletions demo/workspace/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Disposable Workspace stories rendered by the real LoopX backend and UI."""
Loading