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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ release versions will follow [Semantic Versioning](https://semver.org/spec/v2.0.

## [Unreleased]

### Added

- Unified workspace summaries show sessions, net Git changes, recent commits, and
linked pull requests in the existing Summary panel. Change details open in the
current conversation's right review drawer.

### Changed

- Simplified the public README and added source setup, troubleshooting, and platform
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ runner integrations live under `packages/`.

- [Contributing](CONTRIBUTING.md): setup, focused checks, and pull request expectations.
- [Architecture](docs/architecture.md) and [Daemon API](docs/daemon-api.md): system boundaries.
- [Work summary](docs/features/work-summary.md) (Chinese): shared session, change, commit, and pull request summaries.
- [Design index](docs/design/README.md) and [Implementation Status](docs/design/implementation-status.md): maintained contracts and acceptance gates.
- [Provider adapters](docs/provider-adapters.md) and [Pi integration](docs/pi-coding-agent-integration.md): model and runtime integration.
- [Release policy](docs/release/README.md), [Changelog](CHANGELOG.md), and [Third-party notices](THIRD_PARTY_NOTICES.md): publication and licensing.
Expand Down
1 change: 1 addition & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ corepack pnpm --filter @ling-stack/desktop dev
- [贡献指南](CONTRIBUTING.md):开发环境、检查与 PR 要求。
- [README 翻译维护](CONTRIBUTING.md#readme-translations):欢迎校对译文,并同步命令、功能范围与安全说明。
- [架构](docs/architecture.md)与[设计索引](docs/design/README.md):模块边界、契约与验收状态。
- [工作摘要](docs/features/work-summary.md):会话、净变更、最近提交与 PR 的统一摘要及右侧审查入口。
- [供应商适配](docs/provider-adapters.md)与[Pi 集成](docs/pi-coding-agent-integration.md):模型和运行时接入。
- [更新日志](CHANGELOG.md)、[发布政策](docs/release/README.md)与[第三方声明](THIRD_PARTY_NOTICES.md)。

Expand Down
10 changes: 5 additions & 5 deletions apps/web/e2e/app.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,15 +387,15 @@ test("recent conversations only show records created from the conversation home"
await expect(conversationToolbar.locator(".workspace-toolbar-action-label")).toHaveCount(0);
await expect(conversationToolbar.getByRole("button", { name: "Search" })).toHaveClass(/workspace-toolbar-icon-button/);
await expect(conversationToolbar.getByRole("button", { name: "Refresh" })).toHaveCount(0);
await expect(conversationToolbar.getByRole("button", { name: "Summary" })).toHaveClass(/workspace-toolbar-icon-button/);
await expect(conversationToolbar.getByRole("button", { name: "Summary", exact: true })).toHaveClass(/workspace-toolbar-icon-button/);
await expect(conversationToolbar.getByRole("button", { name: "Terminal" })).toHaveClass(/workspace-toolbar-icon-button/);
await expect(conversationToolbar.getByRole("button", { name: /^Review/ })).toHaveCount(0);
await expect(conversationToolbar.getByRole("button", { name: /^Evidence/ })).toHaveCount(0);
await expect(conversationToolbar.getByRole("button", { name: /^Files/ })).toHaveCount(0);
const conversationDetailsButtonBox = await conversationToolbar.getByRole("button", { name: "Show details" }).boundingBox();
expect(conversationDetailsButtonBox?.width ?? 0).toBeGreaterThanOrEqual(32);
expect(conversationDetailsButtonBox?.height ?? 0).toBeGreaterThanOrEqual(32);
await conversationToolbar.getByRole("button", { name: "Summary" }).click();
await conversationToolbar.getByRole("button", { name: "Summary", exact: true }).click();
await expect(page.locator(".normal-conversation-summary-popover")).toContainText("Workspace");
await conversationToolbar.getByRole("button", { name: "Show details" }).click();
const conversationDock = page.getByLabel("Conversation workspace dock");
Expand All @@ -415,19 +415,19 @@ test("recent conversations only show records created from the conversation home"
const dockFrameBox = await page.locator(".normal-conversation-right-dock-frame").boundingBox();
return summaryBox && dockFrameBox ? summaryBox.x + summaryBox.width <= dockFrameBox.x + 1 : false;
}).toBe(true);
await conversationToolbar.getByRole("button", { name: "Summary" }).click();
await conversationToolbar.getByRole("button", { name: "Summary", exact: true }).click();
await expect(conversationPage).toHaveClass(/summary-panel-collapsed/);
await expect(conversationSummaryPanel).toHaveClass(/is-popover-collapsed/);
await expect(conversationSummaryPanel).toHaveCount(0);
await conversationToolbar.getByRole("button", { name: "Summary" }).click();
await conversationToolbar.getByRole("button", { name: "Summary", exact: true }).click();
await expect(conversationSummaryPanel).toBeVisible();
await conversationToolbar.getByRole("button", { name: "Hide details" }).click();
const conversationTopSummary = page.locator(".normal-conversation-summary-popover");
await expect(conversationTopSummary).toBeVisible();
await expect(conversationTopSummary).toHaveAttribute("data-summary-placement-motion", "main->header");
await expect(conversationDock).toHaveCount(0);
await expect(conversationTopSummary).toContainText("Workspace");
await conversationToolbar.getByRole("button", { name: "Summary" }).click();
await conversationToolbar.getByRole("button", { name: "Summary", exact: true }).click();
await conversationToolbar.getByRole("button", { name: "Terminal" }).click();
await expect(page.getByLabel("Terminal session")).toBeVisible();
await page.getByRole("button", { name: "Close terminal panel" }).click();
Expand Down
25 changes: 18 additions & 7 deletions apps/web/e2e/channel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1081,7 +1081,7 @@ test("progress popover shows active context source groups and opens source detai
await expect(page.locator('[data-conversation-timeline-kind="subAgentActivity"]')).toHaveCount(0);
await expect(workflowEvent).toContainText("Engineer is working on Render source summary. 0 of 1 steps complete.");

await page.getByLabel("Workspace task toolbar").getByRole("button", { name: "Summary" }).click();
await page.getByLabel("Workspace task toolbar").getByRole("button", { name: "Summary", exact: true }).click();

const popover = page.locator(".channel-progress-popover");
await expect(popover).toBeVisible();
Expand Down Expand Up @@ -1355,6 +1355,15 @@ test("channel environment surface shows read-only context and opens review", asy
};

await mockDaemon(page, state);
await page.route(/\/workspace\/work-summary(?:\?.*)?$/, async (route) => {
const worktreeCwd = `${cwd}/.ling-stack/worktrees/channels/channel_engineering`;
await route.fulfill({ json: {
cwd,
sessions: [{ id: "channel:channel_engineering", channelId: "channel_engineering", title: "Engineering", status: "open", surface: "channel", workspaceKey: worktreeCwd }],
workspaces: [{ key: worktreeCwd, cwd: worktreeCwd, state: "ready", branch: "ling/channel-environment", changedFiles: 1, additions: 12, deletions: 3, binaryFiles: 0, commits: [], partial: false, checkedAt: "2026-06-29T00:01:00.000Z" }],
pullRequests: [], truncated: false, checkedAt: "2026-06-29T00:01:00.000Z",
} });
});
await page.route(/\/workspace\/changes(?:\?.*)?$/, async (route) => {
const worktreeCwd = `${cwd}/.ling-stack/worktrees/channels/channel_engineering`;
await route.fulfill({
Expand Down Expand Up @@ -1414,7 +1423,7 @@ test("channel environment surface shows read-only context and opens review", asy
};
});

const summaryButton = page.getByLabel("Workspace task toolbar").getByRole("button", { name: "Summary" });
const summaryButton = page.getByLabel("Workspace task toolbar").getByRole("button", { name: "Summary", exact: true });
await summaryButton.click();
const popover = page.locator(".channel-progress-popover");
await expect(popover).toBeVisible();
Expand All @@ -1426,9 +1435,11 @@ test("channel environment surface shows read-only context and opens review", asy
await expect(popover).toContainText("Local environment");
await expect(popover).toContainText("Working branch");
await expect(popover).toContainText("Commit or push");
await expect(popover).toContainText("1 changed file");
await expect(popover).toContainText("+12");
await expect(popover).toContainText("-3");
const summaryChanges = popover.getByRole("button", { name: /Changes · 1/u });
await expect(summaryChanges).toHaveCount(1);
await expect(summaryChanges).toContainText("+12");
await expect(summaryChanges).toContainText("−3");
await expect(popover.locator(".attention-environment-section")).not.toContainText("Changes");
await popover.getByRole("button", { name: "Local environment" }).click();
const quickEnvironmentPage = page.locator(".channel-environment-page");
await expect(quickEnvironmentPage).toBeVisible();
Expand Down Expand Up @@ -1728,7 +1739,7 @@ test("channel environment labels review-only items as review instead of changes"
await page.goto("/");
await openEngineeringChannel(page);

await page.getByLabel("Workspace task toolbar").getByRole("button", { name: "Summary" }).click();
await page.getByLabel("Workspace task toolbar").getByRole("button", { name: "Summary", exact: true }).click();
const environmentSection = page.locator(".channel-progress-popover .attention-environment-section");
await expect(environmentSection).toBeVisible();
await expect(environmentSection).toContainText("Review");
Expand All @@ -1750,7 +1761,7 @@ test("channel environment shows clean git state without review rows", async ({ p
await page.goto("/");
await openEngineeringChannel(page);

await page.getByLabel("Workspace task toolbar").getByRole("button", { name: "Summary" }).click();
await page.getByLabel("Workspace task toolbar").getByRole("button", { name: "Summary", exact: true }).click();
const environmentSection = page.locator(".channel-progress-popover .attention-environment-section");
await expect(environmentSection).toBeVisible();
await expect(environmentSection).not.toContainText("Changes");
Expand Down
4 changes: 2 additions & 2 deletions apps/web/e2e/desktop-computer-use-bridge.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,7 +580,7 @@ test("reports teammate-direct composer, summary, dock, and terminal obstacles",
await expect(dock).toHaveAttribute("data-computer-use-obstacle", "true");
await expect(dock).toHaveAttribute("data-computer-use-obstacle-kind", "right-dock");

await header.getByRole("button", { name: "Summary" }).click();
await header.getByRole("button", { name: "Summary", exact: true }).click();
const summary = page.locator(".teammate-private-summary-panel");
await expect(summary).toHaveAttribute("data-computer-use-obstacle", "true");
await expect(summary).toHaveAttribute("data-computer-use-obstacle-kind", "summary");
Expand All @@ -595,7 +595,7 @@ test("reports teammate-direct composer, summary, dock, and terminal obstacles",
await expect(terminal).toHaveAttribute("data-computer-use-obstacle", "true");
await expect(terminal).toHaveAttribute("data-computer-use-obstacle-kind", "terminal");
await expect(summary).toBeVisible();
await expect(header.getByRole("button", { name: "Summary" })).toHaveAttribute("aria-pressed", "true");
await expect(header.getByRole("button", { name: "Summary", exact: true })).toHaveAttribute("aria-pressed", "true");

await expect.poll(async () => {
const layout = (await desktopControlSnapshot(page)).latestHostLayout;
Expand Down
10 changes: 5 additions & 5 deletions apps/web/e2e/teammates.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1211,14 +1211,14 @@ test("AI teammate private workspace sends blocked chat replies and persists sett
await expect(privateToolbar.locator(".workspace-toolbar-action-label")).toHaveCount(0);
await expect(privateToolbar.getByRole("button", { name: "Search" })).toHaveClass(/workspace-toolbar-icon-button/);
await expect(privateToolbar.getByRole("button", { name: "Refresh" })).toHaveCount(0);
await expect(privateToolbar.getByRole("button", { name: "Summary" })).toHaveClass(/workspace-toolbar-icon-button/);
await expect(privateToolbar.getByRole("button", { name: "Summary", exact: true })).toHaveClass(/workspace-toolbar-icon-button/);
await expect(privateToolbar.getByRole("button", { name: "Terminal" })).toHaveClass(/workspace-toolbar-icon-button/);
await expect(privateToolbar.getByRole("button", { name: "Settings" })).toHaveCount(0);
await expect(privateToolbar.getByRole("button", { name: /^Review/ })).toHaveCount(0);
await expect(privateToolbar.getByRole("button", { name: /^Evidence/ })).toHaveCount(0);
await expect(privateToolbar.getByRole("button", { name: /^Files/ })).toHaveCount(0);
await expect(privateToolbar.getByRole("button", { name: /^Private attachments:/ })).toHaveCount(0);
await privateToolbar.getByRole("button", { name: "Summary" }).click();
await privateToolbar.getByRole("button", { name: "Summary", exact: true }).click();
await expect(page.locator(".teammate-chat-summary-popover")).toContainText("Private workspace");
await expect(page.locator(".teammate-chat-summary-popover")).toContainText("Memory on");
await expect(page.getByLabel("Conversation context")).toHaveCount(0);
Expand All @@ -1240,19 +1240,19 @@ test("AI teammate private workspace sends blocked chat replies and persists sett
const dockFrameBox = await page.locator(".teammate-private-right-dock-frame").boundingBox();
return summaryBox && dockFrameBox ? summaryBox.x + summaryBox.width <= dockFrameBox.x + 1 : false;
}).toBe(true);
await privateToolbar.getByRole("button", { name: "Summary" }).click();
await privateToolbar.getByRole("button", { name: "Summary", exact: true }).click();
await expect(privateChatSurface).toHaveClass(/summary-panel-collapsed/);
await expect(privateSummaryPanel).toHaveClass(/is-popover-collapsed/);
await expect(privateSummaryPanel).toHaveCount(0);
await privateToolbar.getByRole("button", { name: "Summary" }).click();
await privateToolbar.getByRole("button", { name: "Summary", exact: true }).click();
await expect(privateSummaryPanel).toBeVisible();
await privateToolbar.getByRole("button", { name: "Hide details" }).click();
const privateTopSummary = page.locator(".teammate-chat-summary-popover");
await expect(privateTopSummary).toBeVisible();
await expect(privateTopSummary).toHaveAttribute("data-summary-placement-motion", "main->header");
await expect(privateDock).toHaveCount(0);
await expect(privateTopSummary).toContainText("Private workspace");
await privateToolbar.getByRole("button", { name: "Summary" }).click();
await privateToolbar.getByRole("button", { name: "Summary", exact: true }).click();
await privateToolbar.getByRole("button", { name: "Terminal" }).click();
await expect(page.getByLabel("Terminal session")).toBeVisible();
await page.getByRole("button", { name: "Close terminal panel" }).click();
Expand Down
2 changes: 1 addition & 1 deletion apps/web/e2e/ui2-channel-header-search.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ test("channel header refresh and search keep local state and overlays independen
const header = page.locator(".task-workbench-header");
const toolbar = page.getByRole("toolbar", { name: "Workspace task toolbar" });
const searchButton = toolbar.getByRole("button", { name: "Search" });
const summaryButton = toolbar.getByRole("button", { name: "Summary" });
const summaryButton = toolbar.getByRole("button", { name: "Summary", exact: true });
const teammateButton = toolbar.getByRole("button", { name: "AI teammate management" });

await summaryButton.click();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ test("T13-O.25-C dark reduced-motion Summary yields to the Channel Dock without
await openEngineeringChannel(page);

const toolbar = page.getByRole("toolbar", { name: "Workspace task toolbar" });
const summaryButton = toolbar.getByRole("button", { name: "Summary" });
const summaryButton = toolbar.getByRole("button", { name: "Summary", exact: true });
const detailsButton = toolbar.getByRole("button", { name: "Show details" });
await assertMinimumHitAreas(toolbar.locator("button:visible"));
await assertFocusedControlContained(detailsButton, toolbar);
Expand Down
Loading
Loading