mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-07 16:19:09 +00:00
Merge branch 'dev' into feat/readd-fable5-bedrock
This commit is contained in:
commit
d306a5a35d
10 changed files with 236 additions and 91 deletions
2
.github/workflows/pr_lint.yml
vendored
2
.github/workflows/pr_lint.yml
vendored
|
|
@ -41,6 +41,8 @@ jobs:
|
||||||
deploy
|
deploy
|
||||||
ci
|
ci
|
||||||
infra
|
infra
|
||||||
|
dashboard
|
||||||
|
triage
|
||||||
requireScope: false
|
requireScope: false
|
||||||
ignoreLabels: |
|
ignoreLabels: |
|
||||||
ignore-lint-pr-title
|
ignore-lint-pr-title
|
||||||
|
|
|
||||||
|
|
@ -378,6 +378,14 @@ def _thread_summary(
|
||||||
thread_id = thread.get("thread_id") or thread.get("id")
|
thread_id = thread.get("thread_id") or thread.get("id")
|
||||||
trace_url = get_langsmith_trace_url(thread_id) if isinstance(thread_id, str) else None
|
trace_url = get_langsmith_trace_url(thread_id) if isinstance(thread_id, str) else None
|
||||||
|
|
||||||
|
raw_sandbox_id = metadata.get("sandbox_id")
|
||||||
|
# "__creating__" is the in-flight sentinel written before the real id lands.
|
||||||
|
sandbox_id = (
|
||||||
|
raw_sandbox_id
|
||||||
|
if isinstance(raw_sandbox_id, str) and raw_sandbox_id and raw_sandbox_id != "__creating__"
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
summary: dict[str, Any] = {
|
summary: dict[str, Any] = {
|
||||||
"id": thread_id,
|
"id": thread_id,
|
||||||
"title": title,
|
"title": title,
|
||||||
|
|
@ -406,6 +414,7 @@ def _thread_summary(
|
||||||
"updatedAt": int(updated_at) if isinstance(updated_at, (int, float)) else _now_ms(),
|
"updatedAt": int(updated_at) if isinstance(updated_at, (int, float)) else _now_ms(),
|
||||||
"isOwner": (_user_owns_thread(metadata, owner_login, owner_email) if owner_login else True),
|
"isOwner": (_user_owns_thread(metadata, owner_login, owner_email) if owner_login else True),
|
||||||
"traceUrl": trace_url,
|
"traceUrl": trace_url,
|
||||||
|
"sandboxId": sandbox_id,
|
||||||
}
|
}
|
||||||
if isinstance(pr_number, int) and isinstance(pr_url, str):
|
if isinstance(pr_number, int) and isinstance(pr_url, str):
|
||||||
summary["pr"] = {
|
summary["pr"] = {
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@
|
||||||
{"sha": "ab4eea4b", "pr": 1663, "subject": "chore(deps): bump the major group across 1 directory with 3 updates (#1663)", "disposition": "wont-merge", "reason": "already in dev — CI already on checkout@v7/setup-node@v6/upload-artifact@v7", "branch": "", "local_sha": null, "updated": "2026-07-08T20:14:42Z"}
|
{"sha": "ab4eea4b", "pr": 1663, "subject": "chore(deps): bump the major group across 1 directory with 3 updates (#1663)", "disposition": "wont-merge", "reason": "already in dev — CI already on checkout@v7/setup-node@v6/upload-artifact@v7", "branch": "", "local_sha": null, "updated": "2026-07-08T20:14:42Z"}
|
||||||
{"sha": "2bf207fb", "pr": 1664, "subject": "chore(deps): bump python in the minor-and-patch group (#1664)", "disposition": "wont-merge", "reason": "already in dev — Dockerfile already at python 3.14.6-slim-trixie", "branch": "", "local_sha": null, "updated": "2026-07-08T20:14:42Z"}
|
{"sha": "2bf207fb", "pr": 1664, "subject": "chore(deps): bump python in the minor-and-patch group (#1664)", "disposition": "wont-merge", "reason": "already in dev — Dockerfile already at python 3.14.6-slim-trixie", "branch": "", "local_sha": null, "updated": "2026-07-08T20:14:42Z"}
|
||||||
{"sha": "13b40113", "pr": 1666, "subject": "chore(deps): bump cryptography from 48.0.1 to 49.0.0 in the major group (#1666)", "disposition": "wont-merge", "reason": "already in dev — same 48->49 cryptography bump dev did via #105", "branch": "", "local_sha": null, "updated": "2026-07-08T20:14:42Z"}
|
{"sha": "13b40113", "pr": 1666, "subject": "chore(deps): bump cryptography from 48.0.1 to 49.0.0 in the major group (#1666)", "disposition": "wont-merge", "reason": "already in dev — same 48->49 cryptography bump dev did via #105", "branch": "", "local_sha": null, "updated": "2026-07-08T20:14:42Z"}
|
||||||
{"sha": "fbc6de85", "pr": 1667, "subject": "chore(deps): bump fireworks-ai from 1.2.0a75 to 1.2.0a86 (#1667)", "disposition": "deferred", "reason": "conflicts on pick — dev diverged to fireworks-ai a85; a86 still wanted, needs manual bump + uv lock", "branch": "deps", "local_sha": null, "updated": "2026-07-08T23:17:35Z"}
|
{"sha": "fbc6de85", "pr": 1667, "subject": "chore(deps): bump fireworks-ai from 1.2.0a75 to 1.2.0a86 (#1667)", "disposition": "landed", "reason": "Superseded by #158 (fireworks-ai a85 -> a88, efeb6b12); dev already exceeds a86. No port needed.", "branch": "deps", "local_sha": null, "updated": "2026-07-10T16:38:22Z"}
|
||||||
{"sha": "290d0fee", "pr": 1669, "subject": "chore(deps): update langgraph-cli[inmem] requirement (#1669)", "disposition": "wont-merge", "reason": "already in dev — langgraph-cli[inmem] at 0.4.30", "branch": "", "local_sha": null, "updated": "2026-07-08T20:14:42Z"}
|
{"sha": "290d0fee", "pr": 1669, "subject": "chore(deps): update langgraph-cli[inmem] requirement (#1669)", "disposition": "wont-merge", "reason": "already in dev — langgraph-cli[inmem] at 0.4.30", "branch": "", "local_sha": null, "updated": "2026-07-08T20:14:42Z"}
|
||||||
{"sha": "c9f6dd86", "pr": 1694, "subject": "fix: fall back on model stream transport errors (#1694)", "disposition": "landed", "reason": "adds httpx.TransportError to transient set; small conflict w/ dev's diverged Bedrock fallback", "branch": "model-fallback", "local_sha": null, "updated": "2026-07-09T18:59:20Z"}
|
{"sha": "c9f6dd86", "pr": 1694, "subject": "fix: fall back on model stream transport errors (#1694)", "disposition": "landed", "reason": "adds httpx.TransportError to transient set; small conflict w/ dev's diverged Bedrock fallback", "branch": "model-fallback", "local_sha": null, "updated": "2026-07-09T18:59:20Z"}
|
||||||
{"sha": "c9a9a7cd", "pr": 1695, "subject": "fix: retry model fallback exhaustion (#1695)", "disposition": "landed", "reason": "alternating retry+backoff rewrite; reconcile by hand w/ dev's Bedrock + sync wrap path", "branch": "model-fallback", "local_sha": null, "updated": "2026-07-09T18:59:20Z"}
|
{"sha": "c9a9a7cd", "pr": 1695, "subject": "fix: retry model fallback exhaustion (#1695)", "disposition": "landed", "reason": "alternating retry+backoff rewrite; reconcile by hand w/ dev's Bedrock + sync wrap path", "branch": "model-fallback", "local_sha": null, "updated": "2026-07-09T18:59:20Z"}
|
||||||
|
|
@ -74,7 +74,7 @@
|
||||||
{"sha": "bb104d93", "pr": 1679, "subject": "fix: submit plan comments with cmd enter (#1679)", "disposition": "landed", "reason": "applies clean but edits fork-diverged PlanReview.tsx (#130); needs UI/e2e validation — separate PR", "branch": "plan-approval", "local_sha": null, "updated": "2026-07-09T17:10:22Z"}
|
{"sha": "bb104d93", "pr": 1679, "subject": "fix: submit plan comments with cmd enter (#1679)", "disposition": "landed", "reason": "applies clean but edits fork-diverged PlanReview.tsx (#130); needs UI/e2e validation — separate PR", "branch": "plan-approval", "local_sha": null, "updated": "2026-07-09T17:10:22Z"}
|
||||||
{"sha": "304032fa", "pr": 1680, "subject": "chore: clarify question answering prompt (#1680)", "disposition": "deferred", "reason": "reword Slack info-only answer guidance; conflicts w/ fork's customized Slack prompt", "branch": "prompt-tweaks", "local_sha": null, "updated": "2026-07-08T20:14:42Z"}
|
{"sha": "304032fa", "pr": 1680, "subject": "chore: clarify question answering prompt (#1680)", "disposition": "deferred", "reason": "reword Slack info-only answer guidance; conflicts w/ fork's customized Slack prompt", "branch": "prompt-tweaks", "local_sha": null, "updated": "2026-07-08T20:14:42Z"}
|
||||||
{"sha": "5003c953", "pr": 1683, "subject": "feat: open Linear-triggered PRs as the triggering user (#1683)", "disposition": "deferred", "reason": "FLAG-HUMAN: adds linear to resolve_github_token per-user OAuth branch (auth surface); depends on #1626 linear.py", "branch": "linear-pr-as-user", "local_sha": null, "updated": "2026-07-08T20:14:42Z"}
|
{"sha": "5003c953", "pr": 1683, "subject": "feat: open Linear-triggered PRs as the triggering user (#1683)", "disposition": "deferred", "reason": "FLAG-HUMAN: adds linear to resolve_github_token per-user OAuth branch (auth surface); depends on #1626 linear.py", "branch": "linear-pr-as-user", "local_sha": null, "updated": "2026-07-08T20:14:42Z"}
|
||||||
{"sha": "feb7ac98", "pr": 1689, "subject": "feat(web): surface thread sandbox ID with touch-friendly menu (#1689)", "disposition": "deferred", "reason": "applies clean but frontend<->backend contract (thread_api->queries->types->sidebar); needs UI build + e2e validation — separate PR", "branch": "dashboard-ui", "local_sha": null, "updated": "2026-07-08T23:17:35Z"}
|
{"sha": "feb7ac98", "pr": 1689, "subject": "feat(web): surface thread sandbox ID with touch-friendly menu (#1689)", "disposition": "landed", "reason": "Ported to dev via feat/thread-sandbox-id-sidebar.", "branch": "dashboard-ui", "local_sha": null, "updated": "2026-07-10T16:48:54Z"}
|
||||||
{"sha": "7f7af715", "pr": 1684, "subject": "feat: auto-load scoped AGENTS on reads (#1684)", "disposition": "landed", "reason": "ported in #129 (SubdirAgentsReadMiddleware)", "branch": "subdir-agents", "local_sha": null, "updated": "2026-07-08T22:58:22Z"}
|
{"sha": "7f7af715", "pr": 1684, "subject": "feat: auto-load scoped AGENTS on reads (#1684)", "disposition": "landed", "reason": "ported in #129 (SubdirAgentsReadMiddleware)", "branch": "subdir-agents", "local_sha": null, "updated": "2026-07-08T22:58:22Z"}
|
||||||
{"sha": "88b62322", "pr": 1685, "subject": "feat: add platform issue reporting tool (#1685)", "disposition": "landed", "reason": "ported in #129 (report_platform_issue tool)", "branch": "small-tools", "local_sha": null, "updated": "2026-07-08T22:58:22Z"}
|
{"sha": "88b62322", "pr": 1685, "subject": "feat: add platform issue reporting tool (#1685)", "disposition": "landed", "reason": "ported in #129 (report_platform_issue tool)", "branch": "small-tools", "local_sha": null, "updated": "2026-07-08T22:58:22Z"}
|
||||||
{"sha": "90cb6caa", "pr": 1681, "subject": "feat: terse Slack replies, share long content via plan-review page (#1681)", "disposition": "landed", "reason": "terse Slack + long-content-via-plan-page; conflicts w/ fork prompt + diverged plan stack", "branch": "plan-approval", "local_sha": null, "updated": "2026-07-09T17:10:22Z"}
|
{"sha": "90cb6caa", "pr": 1681, "subject": "feat: terse Slack replies, share long content via plan-review page (#1681)", "disposition": "landed", "reason": "terse Slack + long-content-via-plan-page; conflicts w/ fork prompt + diverged plan stack", "branch": "plan-approval", "local_sha": null, "updated": "2026-07-09T17:10:22Z"}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ Rows key on the **upstream SHA** (stable across local cherry-picks). Deferred ro
|
||||||
| `4f913198` | #1647 | widen split review diffs | Landed | already present in dev; empty pick confirmed in #127 | reviewer-misc |
|
| `4f913198` | #1647 | widen split review diffs | Landed | already present in dev; empty pick confirmed in #127 | reviewer-misc |
|
||||||
| `20f63e8c` | #1646 | install missing deps before verification | Landed | already in dev via #81 (upstream-sync); ledger was stale | prompt-tweaks |
|
| `20f63e8c` | #1646 | install missing deps before verification | Landed | already in dev via #81 (upstream-sync); ledger was stale | prompt-tweaks |
|
||||||
| `2f237b53` | #1626 | fall back to vision model for image threads | Landed | ported (adapted to Bedrock/Fireworks vision) in #128 | gateway-routing |
|
| `2f237b53` | #1626 | fall back to vision model for image threads | Landed | ported (adapted to Bedrock/Fireworks vision) in #128 | gateway-routing |
|
||||||
|
| `fbc6de85` | #1667 | chore(deps): bump fireworks-ai from 1.2.0a75 to 1.2.0a86 (#1667) | Landed | Superseded by #158 (fireworks-ai a85 -> a88, efeb6b12); dev already exceeds a86. No port needed. | deps |
|
||||||
| `c9f6dd86` | #1694 | fix: fall back on model stream transport errors (#1694) | Landed | adds httpx.TransportError to transient set; small conflict w/ dev's diverged Bedrock fallback | model-fallback |
|
| `c9f6dd86` | #1694 | fix: fall back on model stream transport errors (#1694) | Landed | adds httpx.TransportError to transient set; small conflict w/ dev's diverged Bedrock fallback | model-fallback |
|
||||||
| `c9a9a7cd` | #1695 | fix: retry model fallback exhaustion (#1695) | Landed | alternating retry+backoff rewrite; reconcile by hand w/ dev's Bedrock + sync wrap path | model-fallback |
|
| `c9a9a7cd` | #1695 | fix: retry model fallback exhaustion (#1695) | Landed | alternating retry+backoff rewrite; reconcile by hand w/ dev's Bedrock + sync wrap path | model-fallback |
|
||||||
| `e5dbc788` | #1696 | fix: Harden durable agent runs (#1696) | Landed | large durable-run hardening; 3 new modules dev lacks; rewrites fork dispatch/completion | durable-dispatch |
|
| `e5dbc788` | #1696 | fix: Harden durable agent runs (#1696) | Landed | large durable-run hardening; 3 new modules dev lacks; rewrites fork dispatch/completion | durable-dispatch |
|
||||||
|
|
@ -55,6 +56,7 @@ Rows key on the **upstream SHA** (stable across local cherry-picks). Deferred ro
|
||||||
| `3dbc0282` | #1676 | fix: preserve plan redirects after login (#1676) | Landed | FLAG-HUMAN: follow-on to landed #1668 refining sanitize_redirect_to (open-redirect auth surface); not a dup | plan-approval |
|
| `3dbc0282` | #1676 | fix: preserve plan redirects after login (#1676) | Landed | FLAG-HUMAN: follow-on to landed #1668 refining sanitize_redirect_to (open-redirect auth surface); not a dup | plan-approval |
|
||||||
| `c75cbb1f` | #1677 | feat: re-add Fable 5 with an admin toggle to disable it (#1677) | Landed | Ported + Bedrock-converted onto dev via feat/readd-fable5-bedrock; anthropic: Fable ID mapped to bedrock_converse:us.anthropic.claude-fable-5. | fable-admin-toggle |
|
| `c75cbb1f` | #1677 | feat: re-add Fable 5 with an admin toggle to disable it (#1677) | Landed | Ported + Bedrock-converted onto dev via feat/readd-fable5-bedrock; anthropic: Fable ID mapped to bedrock_converse:us.anthropic.claude-fable-5. | fable-admin-toggle |
|
||||||
| `bb104d93` | #1679 | fix: submit plan comments with cmd enter (#1679) | Landed | applies clean but edits fork-diverged PlanReview.tsx (#130); needs UI/e2e validation — separate PR | plan-approval |
|
| `bb104d93` | #1679 | fix: submit plan comments with cmd enter (#1679) | Landed | applies clean but edits fork-diverged PlanReview.tsx (#130); needs UI/e2e validation — separate PR | plan-approval |
|
||||||
|
| `feb7ac98` | #1689 | feat(web): surface thread sandbox ID with touch-friendly menu (#1689) | Landed | Ported to dev via feat/thread-sandbox-id-sidebar. | dashboard-ui |
|
||||||
| `7f7af715` | #1684 | feat: auto-load scoped AGENTS on reads (#1684) | Landed | ported in #129 (SubdirAgentsReadMiddleware) | subdir-agents |
|
| `7f7af715` | #1684 | feat: auto-load scoped AGENTS on reads (#1684) | Landed | ported in #129 (SubdirAgentsReadMiddleware) | subdir-agents |
|
||||||
| `88b62322` | #1685 | feat: add platform issue reporting tool (#1685) | Landed | ported in #129 (report_platform_issue tool) | small-tools |
|
| `88b62322` | #1685 | feat: add platform issue reporting tool (#1685) | Landed | ported in #129 (report_platform_issue tool) | small-tools |
|
||||||
| `90cb6caa` | #1681 | feat: terse Slack replies, share long content via plan-review page (#1681) | Landed | terse Slack + long-content-via-plan-page; conflicts w/ fork prompt + diverged plan stack | plan-approval |
|
| `90cb6caa` | #1681 | feat: terse Slack replies, share long content via plan-review page (#1681) | Landed | terse Slack + long-content-via-plan-page; conflicts w/ fork prompt + diverged plan stack | plan-approval |
|
||||||
|
|
@ -87,11 +89,9 @@ Rows key on the **upstream SHA** (stable across local cherry-picks). Deferred ro
|
||||||
| `c0a7e93e` | #1691 | fix: reconnect sandbox backend on resumed runs (#1691) | Deferred | reconnect proxy (has_backend/reconnect); assumes async create_sandbox | sandbox-refactor |
|
| `c0a7e93e` | #1691 | fix: reconnect sandbox backend on resumed runs (#1691) | Deferred | reconnect proxy (has_backend/reconnect); assumes async create_sandbox | sandbox-refactor |
|
||||||
| `4f8bc2dd` | #1692 | refactor: simplify open-swe agent sandbox lifecycle (#1692) | Deferred | FLAG-HUMAN: structural rewrite of ensure_sandbox_for_thread (drops __creating__ 4-case sentinel) | sandbox-refactor |
|
| `4f8bc2dd` | #1692 | refactor: simplify open-swe agent sandbox lifecycle (#1692) | Deferred | FLAG-HUMAN: structural rewrite of ensure_sandbox_for_thread (drops __creating__ 4-case sentinel) | sandbox-refactor |
|
||||||
| `48217b68` | #1489 | feat(open-swe): add E2B sandbox provider (#1489) | Deferred | additive E2B provider; separable but ships on the async sandbox.py base | sandbox-refactor |
|
| `48217b68` | #1489 | feat(open-swe): add E2B sandbox provider (#1489) | Deferred | additive E2B provider; separable but ships on the async sandbox.py base | sandbox-refactor |
|
||||||
| `fbc6de85` | #1667 | chore(deps): bump fireworks-ai from 1.2.0a75 to 1.2.0a86 (#1667) | Deferred | conflicts on pick — dev diverged to fireworks-ai a85; a86 still wanted, needs manual bump + uv lock | deps |
|
|
||||||
| `67abf5b0` | #1659 | fix: surface attributed PR creation failures (#1659) | Deferred | PR-attribution-failure guard (new mw, safe imports); heavy conflict on diverged open_pull_request.py | pr-attribution |
|
| `67abf5b0` | #1659 | fix: surface attributed PR creation failures (#1659) | Deferred | PR-attribution-failure guard (new mw, safe imports); heavy conflict on diverged open_pull_request.py | pr-attribution |
|
||||||
| `304032fa` | #1680 | chore: clarify question answering prompt (#1680) | Deferred | reword Slack info-only answer guidance; conflicts w/ fork's customized Slack prompt | prompt-tweaks |
|
| `304032fa` | #1680 | chore: clarify question answering prompt (#1680) | Deferred | reword Slack info-only answer guidance; conflicts w/ fork's customized Slack prompt | prompt-tweaks |
|
||||||
| `5003c953` | #1683 | feat: open Linear-triggered PRs as the triggering user (#1683) | Deferred | FLAG-HUMAN: adds linear to resolve_github_token per-user OAuth branch (auth surface); depends on #1626 linear.py | linear-pr-as-user |
|
| `5003c953` | #1683 | feat: open Linear-triggered PRs as the triggering user (#1683) | Deferred | FLAG-HUMAN: adds linear to resolve_github_token per-user OAuth branch (auth surface); depends on #1626 linear.py | linear-pr-as-user |
|
||||||
| `feb7ac98` | #1689 | feat(web): surface thread sandbox ID with touch-friendly menu (#1689) | Deferred | applies clean but frontend<->backend contract (thread_api->queries->types->sidebar); needs UI build + e2e validation — separate PR | dashboard-ui |
|
|
||||||
| `f53caff1` | #1701 | fix: fall back to core GitHub App scope when optional grants missing (#1701) | Deferred | FLAG-HUMAN: GitHub-App permission-ladder degrade (auth surface); heavy conflict on diverged github_app.py/_resolve_proxy_token | github-app-scope |
|
| `f53caff1` | #1701 | fix: fall back to core GitHub App scope when optional grants missing (#1701) | Deferred | FLAG-HUMAN: GitHub-App permission-ladder degrade (auth surface); heavy conflict on diverged github_app.py/_resolve_proxy_token | github-app-scope |
|
||||||
| `22e024cb` | #1704 | fix: link issue PRs and prompt repo conventions (#1704) | Deferred | issue/PR linking + repo-convention prompt; clean but prompt-conflict risk vs #113 | webhook-issue-linking |
|
| `22e024cb` | #1704 | fix: link issue PRs and prompt repo conventions (#1704) | Deferred | issue/PR linking + repo-convention prompt; clean but prompt-conflict risk vs #113 | webhook-issue-linking |
|
||||||
| `27b0ddeb` | #1708 | feat: add GPT-5.6 OpenAI models (#1708) | Deferred | FLAG-HUMAN: adds OpenAI GPT-5.6 to the model picker; fork's picker is Bedrock/Fireworks-only — needs a product decision before adopting OpenAI models. Gateway (#155) can route OpenAI if adopted. | model-picker |
|
| `27b0ddeb` | #1708 | feat: add GPT-5.6 OpenAI models (#1708) | Deferred | FLAG-HUMAN: adds OpenAI GPT-5.6 to the model picker; fork's picker is Bedrock/Fireworks-only — needs a product decision before adopting OpenAI models. Gateway (#155) can route OpenAI if adopted. | model-picker |
|
||||||
|
|
|
||||||
115
tests/e2e/tests/sandbox_id.spec.ts
Normal file
115
tests/e2e/tests/sandbox_id.spec.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { test, expect, type Locator, type Page } from "@playwright/test";
|
||||||
|
|
||||||
|
// Exercises the sidebar's "Copy sandbox ID" action against the REAL dashboard
|
||||||
|
// UI and a REAL (local-provider) sandbox: the Slack flow runs the agent, which
|
||||||
|
// creates a sandbox and stamps its id into the thread metadata, and the UI
|
||||||
|
// copies that same id. Only the LLM/GitHub/Slack boundaries are faked.
|
||||||
|
const SAME_USER = { login: "alice", email: "alice@example.com" };
|
||||||
|
|
||||||
|
async function loginAs(page: Page, user: { login: string; email: string }) {
|
||||||
|
const res = await page.request.post("/control/login", { data: user });
|
||||||
|
expect(res.ok()).toBeTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drive the Slack flow so the real agent creates a thread + sandbox, then follow
|
||||||
|
// the bot's "Open in Web" link. Returns the created thread id.
|
||||||
|
async function createThreadWithSandbox(page: Page): Promise<string> {
|
||||||
|
await page.goto("/mock/slack");
|
||||||
|
await page.locator("#reset").click();
|
||||||
|
await expect(page.locator("#thread")).toContainText("No messages yet");
|
||||||
|
await page
|
||||||
|
.locator("#text")
|
||||||
|
.fill("<@U0BOT> please add a greet() helper and open a PR");
|
||||||
|
await page.locator("#send").click();
|
||||||
|
await expect(
|
||||||
|
page.locator(".msg.bot").filter({ hasText: "Add greet() helper" }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
const webLink = page.locator('.msg.bot a[href*="/agents/"]').first();
|
||||||
|
await expect(webLink).toBeVisible();
|
||||||
|
await webLink.click();
|
||||||
|
await expect(page).toHaveURL(/\/agents\//);
|
||||||
|
|
||||||
|
const id = new URL(page.url()).pathname.split("/").filter(Boolean).pop();
|
||||||
|
expect(id).toBeTruthy();
|
||||||
|
return id as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyItem = (page: Page) =>
|
||||||
|
page.getByRole("menuitem", { name: "Copy sandbox ID" });
|
||||||
|
|
||||||
|
// The kebab sits beside the row Link (not inside the anchor), so reach it via
|
||||||
|
// their shared wrapper — the Link's parent.
|
||||||
|
const kebabFor = (row: Locator) =>
|
||||||
|
row.locator("..").getByRole("button", { name: "Thread actions" });
|
||||||
|
|
||||||
|
test.describe("thread sandbox id (real dashboard UI)", () => {
|
||||||
|
test("desktop: kebab menu copies the real sandbox id", async ({
|
||||||
|
page,
|
||||||
|
baseURL,
|
||||||
|
}) => {
|
||||||
|
await page
|
||||||
|
.context()
|
||||||
|
.grantPermissions(["clipboard-read", "clipboard-write"], {
|
||||||
|
origin: baseURL,
|
||||||
|
});
|
||||||
|
await loginAs(page, SAME_USER);
|
||||||
|
const threadId = await createThreadWithSandbox(page);
|
||||||
|
|
||||||
|
const row = page.locator(`a[href$="/agents/${threadId}"]`).first();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
// The kebab is revealed on hover on pointer devices.
|
||||||
|
await row.hover();
|
||||||
|
await kebabFor(row).click();
|
||||||
|
|
||||||
|
await expect(copyItem(page)).toBeEnabled();
|
||||||
|
await copyItem(page).click();
|
||||||
|
|
||||||
|
const clip = await page.evaluate(() => navigator.clipboard.readText());
|
||||||
|
expect(clip.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("iPad: kebab copies the sandbox id without navigating", async ({
|
||||||
|
browser,
|
||||||
|
baseURL,
|
||||||
|
}) => {
|
||||||
|
// iPad-class device: Chromium in mobile mode reports (hover: none), which
|
||||||
|
// gates the touch-only kebab. 834px is wider than the 767px mobile
|
||||||
|
// breakpoint, so the sidebar renders inline.
|
||||||
|
const context = await browser.newContext({
|
||||||
|
baseURL,
|
||||||
|
viewport: { width: 834, height: 1112 },
|
||||||
|
isMobile: true,
|
||||||
|
hasTouch: true,
|
||||||
|
});
|
||||||
|
await context.grantPermissions(["clipboard-read", "clipboard-write"], {
|
||||||
|
origin: baseURL,
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
await loginAs(page, SAME_USER);
|
||||||
|
|
||||||
|
// Two threads: we sit on B's page and act on A's sidebar row, so a stray
|
||||||
|
// Link navigation would be observable as a URL change to A.
|
||||||
|
const threadA = await createThreadWithSandbox(page);
|
||||||
|
const threadB = await createThreadWithSandbox(page);
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/agents/${threadB}$`));
|
||||||
|
|
||||||
|
const rowA = page.locator(`a[href$="/agents/${threadA}"]`).first();
|
||||||
|
await expect(rowA).toBeVisible();
|
||||||
|
|
||||||
|
const kebab = kebabFor(rowA);
|
||||||
|
await expect(kebab).toBeVisible();
|
||||||
|
await kebab.tap();
|
||||||
|
|
||||||
|
await expect(copyItem(page)).toBeEnabled();
|
||||||
|
await copyItem(page).tap();
|
||||||
|
|
||||||
|
const clip = await page.evaluate(() => navigator.clipboard.readText());
|
||||||
|
expect(clip.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Tapping the kebab must open the menu, not follow A's Link — we stay on B.
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/agents/${threadB}$`));
|
||||||
|
|
||||||
|
await context.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -317,6 +317,18 @@ def test_thread_summary_omits_pr_when_no_pr_metadata() -> None:
|
||||||
assert "diffStats" not in summary
|
assert "diffStats" not in summary
|
||||||
|
|
||||||
|
|
||||||
|
def test_thread_summary_exposes_sandbox_id() -> None:
|
||||||
|
summary = thread_api._thread_summary(_thread_with_metadata({"sandbox_id": "sb-abc123"}))
|
||||||
|
|
||||||
|
assert summary["sandboxId"] == "sb-abc123"
|
||||||
|
|
||||||
|
|
||||||
|
def test_thread_summary_hides_creating_sandbox_sentinel() -> None:
|
||||||
|
summary = thread_api._thread_summary(_thread_with_metadata({"sandbox_id": "__creating__"}))
|
||||||
|
|
||||||
|
assert summary["sandboxId"] is None
|
||||||
|
|
||||||
|
|
||||||
async def test_recovery_patch_requires_thread_owner(monkeypatch) -> None:
|
async def test_recovery_patch_requires_thread_owner(monkeypatch) -> None:
|
||||||
class FakeThreads:
|
class FakeThreads:
|
||||||
async def get(self, thread_id: str) -> dict[str, object]:
|
async def get(self, thread_id: str) -> dict[str, object]:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ContextMenu } from "@base-ui/react/context-menu"
|
import { Menu } from "@base-ui/react/menu"
|
||||||
import { Dialog } from "@base-ui/react/dialog"
|
import { Dialog } from "@base-ui/react/dialog"
|
||||||
import { Link } from "@tanstack/react-router"
|
import { Link } from "@tanstack/react-router"
|
||||||
import {
|
import {
|
||||||
|
|
@ -10,13 +10,14 @@ import {
|
||||||
ChatCircleIcon,
|
ChatCircleIcon,
|
||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
CircleNotchIcon,
|
CircleNotchIcon,
|
||||||
|
CopyIcon,
|
||||||
|
DotsThreeVerticalIcon,
|
||||||
GitMergeIcon,
|
GitMergeIcon,
|
||||||
GitPullRequestIcon,
|
GitPullRequestIcon,
|
||||||
LightningIcon,
|
LightningIcon,
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
TreeStructureIcon,
|
TreeStructureIcon,
|
||||||
XIcon,
|
|
||||||
} from "@phosphor-icons/react"
|
} from "@phosphor-icons/react"
|
||||||
import { IoLogoGithub, IoLogoSlack } from "react-icons/io5"
|
import { IoLogoGithub, IoLogoSlack } from "react-icons/io5"
|
||||||
import { SiLinear } from "react-icons/si"
|
import { SiLinear } from "react-icons/si"
|
||||||
|
|
@ -360,9 +361,9 @@ function ThreadRow({
|
||||||
const isDeleting =
|
const isDeleting =
|
||||||
deleteThread.isPending && deleteThread.variables === thread.id
|
deleteThread.isPending && deleteThread.variables === thread.id
|
||||||
|
|
||||||
const onDelete = (e: React.MouseEvent) => {
|
const onDelete = (e?: React.MouseEvent) => {
|
||||||
e.preventDefault()
|
e?.preventDefault()
|
||||||
e.stopPropagation()
|
e?.stopPropagation()
|
||||||
if (isDeleting) return
|
if (isDeleting) return
|
||||||
setDeleteOpen(true)
|
setDeleteOpen(true)
|
||||||
}
|
}
|
||||||
|
|
@ -396,25 +397,25 @@ function ThreadRow({
|
||||||
window.open(thread.traceUrl, "_blank", "noopener,noreferrer")
|
window.open(thread.traceUrl, "_blank", "noopener,noreferrer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copySandboxId = () => {
|
||||||
|
if (!thread.sandboxId) return
|
||||||
|
void navigator.clipboard.writeText(thread.sandboxId)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ContextMenu.Root>
|
<div className={cn("group relative mb-0.5", isDeleting && "opacity-50")}>
|
||||||
<ContextMenu.Trigger
|
<Link
|
||||||
render={
|
to="/agents/$threadId"
|
||||||
<Link
|
params={{ threadId: thread.id }}
|
||||||
to="/agents/$threadId"
|
onClick={onNavigate}
|
||||||
params={{ threadId: thread.id }}
|
className={cn(
|
||||||
onClick={onNavigate}
|
"flex items-center gap-2 rounded-lg px-2.5 transition-colors group-hover:pr-8 [@media(hover:none)]:pr-8",
|
||||||
className={cn(
|
compact ? "h-7 gap-1.5" : "h-8",
|
||||||
"group mb-0.5 flex items-center gap-2 rounded-lg px-2.5 transition-colors",
|
isActive
|
||||||
compact ? "h-7 gap-1.5" : "h-8",
|
? "bg-[var(--ui-accent-bubble)] text-[var(--ui-text)]"
|
||||||
isActive
|
: "text-[var(--ui-text-muted)] group-hover:bg-[var(--ui-sidebar-hover)]"
|
||||||
? "bg-[var(--ui-accent-bubble)] text-[var(--ui-text)]"
|
)}
|
||||||
: "text-[var(--ui-text-muted)] hover:bg-[var(--ui-sidebar-hover)]",
|
|
||||||
isDeleting && "opacity-50"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{thread.status === "running" ? (
|
{thread.status === "running" ? (
|
||||||
<CircleNotchIcon
|
<CircleNotchIcon
|
||||||
|
|
@ -461,73 +462,75 @@ function ThreadRow({
|
||||||
{badge}
|
{badge}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{!isReadOnly && (
|
</Link>
|
||||||
<button
|
{/* One actions menu for every input: revealed on hover, kept while
|
||||||
type="button"
|
open, and always shown on devices that can't hover (touch). It sits
|
||||||
aria-label={isResolved ? "Unresolve thread" : "Resolve thread"}
|
outside the Link so opening it never navigates the row. */}
|
||||||
title={isResolved ? "Unresolve thread" : "Resolve thread"}
|
<Menu.Root>
|
||||||
onClick={onToggleResolved}
|
<Menu.Trigger
|
||||||
disabled={resolveThread.isPending}
|
render={
|
||||||
className="hidden size-4 shrink-0 items-center justify-center rounded text-[var(--ui-text-dim)] group-hover:flex hover:bg-[var(--ui-panel-2)] hover:text-[var(--ui-text)]"
|
<button
|
||||||
>
|
type="button"
|
||||||
{isResolved ? (
|
aria-label="Thread actions"
|
||||||
<ArrowCounterClockwiseIcon className="size-3" weight="bold" />
|
className="absolute top-1/2 right-1 hidden size-5 -translate-y-1/2 items-center justify-center rounded text-[var(--ui-text-dim)] group-hover:flex hover:bg-[var(--ui-panel-2)] hover:text-[var(--ui-text)] data-popup-open:flex [@media(hover:none)]:flex"
|
||||||
) : (
|
|
||||||
<CheckCircleIcon className="size-3" weight="bold" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
{!isReadOnly && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label="Delete thread"
|
|
||||||
onClick={onDelete}
|
|
||||||
disabled={isDeleting}
|
|
||||||
className="hidden size-4 shrink-0 items-center justify-center rounded text-[var(--ui-text-dim)] group-hover:flex hover:bg-[var(--ui-panel-2)] hover:text-[var(--ui-text)]"
|
|
||||||
>
|
|
||||||
<XIcon className="size-3" weight="bold" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</ContextMenu.Trigger>
|
|
||||||
<ContextMenu.Portal>
|
|
||||||
<ContextMenu.Positioner className="z-50 outline-none">
|
|
||||||
<ContextMenu.Popup className="min-w-[10rem] overflow-hidden rounded-md border border-[var(--ui-border)] bg-popover p-1 text-popover-foreground shadow-md outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95">
|
|
||||||
<ContextMenu.Item
|
|
||||||
disabled={!thread.traceUrl}
|
|
||||||
onClick={openTrace}
|
|
||||||
className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-xs outline-none select-none data-highlighted:bg-[var(--ui-sidebar-hover)] data-disabled:pointer-events-none data-disabled:opacity-50"
|
|
||||||
>
|
>
|
||||||
<TreeStructureIcon className="size-3.5" />
|
<DotsThreeVerticalIcon className="size-4" weight="bold" />
|
||||||
Open trace
|
</button>
|
||||||
</ContextMenu.Item>
|
}
|
||||||
{!isReadOnly && (
|
/>
|
||||||
<ContextMenu.Item
|
<Menu.Portal>
|
||||||
onClick={() => onToggleResolved()}
|
<Menu.Positioner
|
||||||
disabled={resolveThread.isPending}
|
align="end"
|
||||||
className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-xs outline-none select-none data-highlighted:bg-[var(--ui-sidebar-hover)] data-disabled:pointer-events-none data-disabled:opacity-50"
|
sideOffset={4}
|
||||||
|
className="z-50 outline-none"
|
||||||
|
>
|
||||||
|
<Menu.Popup className="min-w-[10rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md outline-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95">
|
||||||
|
<Menu.Item
|
||||||
|
disabled={!thread.traceUrl}
|
||||||
|
onClick={openTrace}
|
||||||
|
className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-xs outline-none select-none data-highlighted:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{isResolved ? (
|
<TreeStructureIcon className="size-3.5" />
|
||||||
<ArrowCounterClockwiseIcon className="size-3.5" />
|
Open trace
|
||||||
) : (
|
</Menu.Item>
|
||||||
<CheckCircleIcon className="size-3.5" />
|
<Menu.Item
|
||||||
)}
|
disabled={!thread.sandboxId}
|
||||||
{isResolved ? "Unresolve thread" : "Resolve thread"}
|
onClick={copySandboxId}
|
||||||
</ContextMenu.Item>
|
title={thread.sandboxId ?? undefined}
|
||||||
)}
|
className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-xs outline-none select-none data-highlighted:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50"
|
||||||
{!isReadOnly && (
|
|
||||||
<ContextMenu.Item
|
|
||||||
onClick={onDelete}
|
|
||||||
disabled={isDeleting}
|
|
||||||
className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-xs text-[var(--ui-danger)] outline-none select-none data-highlighted:bg-[var(--ui-sidebar-hover)] data-disabled:pointer-events-none data-disabled:opacity-50"
|
|
||||||
>
|
>
|
||||||
<TrashIcon className="size-3.5" />
|
<CopyIcon className="size-3.5" />
|
||||||
Delete thread
|
Copy sandbox ID
|
||||||
</ContextMenu.Item>
|
</Menu.Item>
|
||||||
)}
|
{!isReadOnly && (
|
||||||
</ContextMenu.Popup>
|
<Menu.Item
|
||||||
</ContextMenu.Positioner>
|
onClick={() => onToggleResolved()}
|
||||||
</ContextMenu.Portal>
|
disabled={resolveThread.isPending}
|
||||||
</ContextMenu.Root>
|
className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-xs outline-none select-none data-highlighted:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isResolved ? (
|
||||||
|
<ArrowCounterClockwiseIcon className="size-3.5" />
|
||||||
|
) : (
|
||||||
|
<CheckCircleIcon className="size-3.5" />
|
||||||
|
)}
|
||||||
|
{isResolved ? "Unresolve thread" : "Resolve thread"}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
{!isReadOnly && (
|
||||||
|
<Menu.Item
|
||||||
|
onClick={() => onDelete()}
|
||||||
|
disabled={isDeleting}
|
||||||
|
className="flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-xs text-destructive outline-none select-none data-highlighted:bg-muted data-disabled:pointer-events-none data-disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<TrashIcon className="size-3.5" />
|
||||||
|
Delete thread
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
|
</Menu.Popup>
|
||||||
|
</Menu.Positioner>
|
||||||
|
</Menu.Portal>
|
||||||
|
</Menu.Root>
|
||||||
|
</div>
|
||||||
<Dialog.Root open={deleteOpen} onOpenChange={setDeleteOpen}>
|
<Dialog.Root open={deleteOpen} onOpenChange={setDeleteOpen}>
|
||||||
<Dialog.Portal>
|
<Dialog.Portal>
|
||||||
<Dialog.Backdrop className="fixed inset-0 z-50 bg-black/50 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0" />
|
<Dialog.Backdrop className="fixed inset-0 z-50 bg-black/50 data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0" />
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,7 @@ export function optimisticThread(
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
traceUrl: null,
|
traceUrl: null,
|
||||||
|
sandboxId: null,
|
||||||
messages: message.chunks.length > 0 ? [message] : [],
|
messages: message.chunks.length > 0 ? [message] : [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,7 @@ export interface AgentThread {
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
traceUrl?: string | null
|
traceUrl?: string | null
|
||||||
|
sandboxId?: string | null
|
||||||
messages: Array<Message>
|
messages: Array<Message>
|
||||||
queuedMessages?: Array<QueuedThreadMessage>
|
queuedMessages?: Array<QueuedThreadMessage>
|
||||||
pr?: {
|
pr?: {
|
||||||
|
|
|
||||||
|
|
@ -177,7 +177,9 @@ const config = defineConfig({
|
||||||
registerType: "prompt",
|
registerType: "prompt",
|
||||||
outDir: ".output/public",
|
outDir: ".output/public",
|
||||||
devOptions: {
|
devOptions: {
|
||||||
enabled: true,
|
// Off in dev: the service worker precaches assets and defeats HMR (and
|
||||||
|
// a stale registration lingers per-origin). Dev is HMR-only.
|
||||||
|
enabled: false,
|
||||||
},
|
},
|
||||||
integration: {
|
integration: {
|
||||||
closeBundleOrder: "pre",
|
closeBundleOrder: "pre",
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue