diff --git a/docs/design/artboards/d215/index.html b/docs/design/artboards/d215/index.html index 6a12f416d6..baedabeb38 100644 --- a/docs/design/artboards/d215/index.html +++ b/docs/design/artboards/d215/index.html @@ -749,6 +749,128 @@ .v2-run-trace-link a { font: 500 12.5px/1 var(--f-body); color: var(--se); text-decoration: none; } .v2-run-trace-link span { margin-left: 12px; font-size: 12px; color: var(--mu); } .app.m .v2-run-trace-link { min-height: 44px; } +/* activity: the live v2-activity__* names (V2ActivityPage), redrawn in the connectors family: 1px + bordered cards, ink key for the deciding act, 32px bordered squares for secondary acts, mono meta. + Scoped to .v2-activity, the page's root. New names: __meta, __heading, __start--pending, __start-bar, + __queue-action--reply, __sections--scrolled. */ +.v2-activity { display: flex; flex-direction: column; min-width: 0; } +.v2-activity button { all: unset; box-sizing: border-box; cursor: pointer; } +.v2-activity .info { all: unset; cursor: help; position: relative; display: inline-grid; place-items: center; width: 18px; height: 18px; border-radius: 50%; color: var(--ph); } +.v2-activity .v2-activity__header { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding-bottom: 20px; } +.v2-activity .v2-activity__heading { display: flex; align-items: center; gap: 10px; min-width: 0; } +.v2-activity .v2-activity__title { margin: 0; font: 800 30px/1.05 var(--f-display); letter-spacing: -0.03em; color: var(--ink); } +.v2-activity .v2-activity__meta { font: 500 12px/16px var(--f-mono); color: var(--mu); white-space: nowrap; } +.v2-activity .v2-activity__controls { display: flex; align-items: center; gap: 8px; flex: none; } +.v2-activity .v2-activity__window, .v2-activity .v2-activity__scope { display: flex; gap: 2px; padding: 3px; border-radius: 9px; background: var(--sf); box-shadow: inset 0 0 0 1px var(--d); min-width: 0; } +.v2-activity .v2-activity__window-button, .v2-activity .v2-activity__scope-button { display: inline-flex; align-items: center; height: 28px; padding: 0 10px; border-radius: 7px; font: 600 11.5px/1 var(--f-mono); color: var(--se); white-space: nowrap; } +.v2-activity .v2-activity__window-button { font-family: var(--f-body); font-size: 12.5px; } +.v2-activity .v2-activity__window-button--active, .v2-activity .v2-activity__scope-button--active { background: var(--t); color: var(--ink); } +.v2-activity .v2-activity__compose, .v2-activity .v2-activity__start, .v2-activity .v2-activity__sections { width: 100%; max-width: 760px; margin-left: auto; margin-right: auto; } +.v2-activity .v2-activity__compose { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px; border-radius: 14px; background: var(--sf); box-shadow: inset 0 0 0 1px var(--d); } +.v2-activity .v2-activity__compose-pod { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--mu); min-width: 0; } +.v2-activity .v2-activity__compose-picker { min-width: 0; } +.v2-activity .v2-activity__compose-picker-button { display: inline-flex; align-items: center; height: 30px; max-width: 240px; padding: 0 12px; border-radius: 8px; box-shadow: inset 0 0 0 1px var(--d); font: 600 13px/1 var(--f-body); color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.v2-activity .v2-activity__compose-input { min-height: 44px; font-size: 14px; line-height: 20px; color: var(--ph); } +.v2-activity .v2-activity__compose-foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; } +.v2-activity .v2-activity__compose-hint { font: 500 11.5px/16px var(--f-mono); color: var(--mu); } +.v2-activity .v2-activity__compose-send, .v2-activity .v2-activity__start-cta--current, .v2-activity .v2-activity__queue-actions > button:not([class]) { display: inline-flex; align-items: center; justify-content: center; height: 32px; padding: 0 13px; border-radius: 9px; font: 600 13px/1 var(--f-body); color: var(--on-fill); background: linear-gradient(180deg, var(--fill-a), var(--fill-b)); box-shadow: inset 0 0 0 1px var(--fill-edge); white-space: nowrap; } +/* Get started: one bordered card; ink numbered marks; the current step's key is ink, the rest bordered */ +.v2-activity .v2-activity__start { margin-top: 16px; border-radius: 12px; background: var(--sf); box-shadow: inset 0 0 0 1px var(--d); overflow: hidden; } +.v2-activity .v2-activity__start-head { display: flex; align-items: baseline; gap: 10px; padding: 10px 14px; border-bottom: 1px solid var(--d); } +.v2-activity .v2-activity__start-head h2 { margin: 0; font: 600 14px/20px var(--f-body); letter-spacing: 0; color: var(--ink); } +.v2-activity .v2-activity__start-kicker { font: 500 11px/16px var(--f-mono); color: var(--mu); } +.v2-activity .v2-activity__start-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; gap: 12px; align-items: start; padding: 12px 14px; border-bottom: 1px solid var(--d); } +.v2-activity .v2-activity__start-row:last-child { border-bottom: 0; } +.v2-activity .v2-activity__start-num { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; background: linear-gradient(180deg, var(--fill-a), var(--fill-b)); color: var(--on-fill); font: 500 11px/16px var(--f-mono); } +.v2-activity .v2-activity__start-copy { min-width: 0; } +.v2-activity .v2-activity__start-copy strong { display: block; font: 600 14px/20px var(--f-body); color: var(--ink); } +.v2-activity .v2-activity__start-copy p { margin: 2px 0 0; font-size: 14px; line-height: 20px; color: var(--se); } +.v2-activity .v2-activity__start-act > button:not([class]) { display: inline-flex; align-items: center; height: 32px; padding: 0 12px; border-radius: 9px; box-shadow: inset 0 0 0 1px var(--d); font: 600 13px/1 var(--f-body); color: var(--ink); white-space: nowrap; } +/* Reserved state: same rows, same line boxes, bars in place of words — the card's height is fixed at first paint */ +.v2-activity .v2-activity__start--pending .v2-activity__start-num--pending { background: var(--t); } +.v2-activity .v2-activity__start-bar { display: block; height: 20px; position: relative; } +.v2-activity .v2-activity__start-bar::after { content: ""; position: absolute; left: 0; top: 5px; height: 10px; width: 92%; border-radius: 5px; background: var(--t); } +.v2-activity .v2-activity__start-bar--title + .v2-activity__start-bar { margin-top: 2px; } +.v2-activity .v2-activity__start-bar--title::after { width: 34%; background: var(--d); } +.v2-activity .v2-activity__start-row .v2-activity__start-bar + .v2-activity__start-bar:last-child::after { width: 64%; } +.v2-activity .v2-activity__start-bar--key { width: 132px; height: 32px; } +.v2-activity .v2-activity__start-bar--key::after { top: 0; height: 32px; width: 100%; border-radius: 9px; background: var(--p); box-shadow: inset 0 0 0 1px var(--d); } +.v2-activity--strip { padding: 0; height: 100%; background: var(--g); justify-content: center; } +.v2-activity--strip .v2-activity__start { margin: 0 auto; width: calc(100% - 48px); } +.v2-activity__strips { display: grid; gap: 18px; align-content: start; grid-column: span 2; min-width: 0; } +@media (max-width: 640px) { .v2-activity__strips { grid-column: auto; } } +/* sections */ +.v2-activity .v2-activity__sections { display: grid; gap: 30px; margin-top: 26px; } +.v2-activity .v2-activity__section-heading { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; } +.v2-activity .v2-activity__section h2 { margin: 0; font: 600 15px/20px var(--f-body); letter-spacing: 0; color: var(--ink); } +.v2-activity .v2-activity__count { font: 500 11px/16px var(--f-mono); color: var(--se); } +.v2-activity .v2-activity__empty--plain { padding: 16px 18px; border-radius: 10px; border: 1px dashed var(--b); } +.v2-activity .v2-activity__empty--plain span { font-size: 13px; line-height: 18px; color: var(--se); } +/* Needs you cards */ +.v2-activity .v2-activity__queue { display: grid; gap: 10px; } +.v2-activity .v2-activity__queue-row { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; align-items: start; gap: 12px; padding: 11px 13px; border-radius: 10px; background: var(--sf); box-shadow: inset 0 0 0 1px var(--d); } +.v2-activity .v2-activity__queue-mark { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; } +.v2-activity .v2-activity__queue-mark svg.i { width: 16px; height: 16px; stroke-width: 2; } +.v2-activity .v2-activity__queue-mark--agent { background: linear-gradient(180deg, var(--c-fill-a), var(--c-fill-b)); color: var(--on-c); } +.v2-activity .v2-activity__queue-mark--human { background: var(--t); color: var(--ink); } +.v2-activity .v2-activity__queue-copy { min-width: 0; } +.v2-activity .v2-activity__queue-kind { font: 500 11px/16px var(--f-mono); color: var(--se); } +.v2-activity .v2-activity__queue-topline strong { display: block; font: 600 14px/20px var(--f-body); color: var(--ink); overflow-wrap: anywhere; } +.v2-activity .v2-activity__queue-detail { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-top: 3px; font-size: 14px; line-height: 20px; color: var(--se); cursor: pointer; } +.v2-activity .v2-activity__queue-row--mention .v2-activity__queue-detail { color: var(--ink2); } +.v2-activity .v2-activity__queue-actions { display: flex; align-items: center; justify-content: flex-end; gap: 6px; max-width: 320px; } +.v2-activity .v2-activity__queue-action--bordered:not(.v2-activity__queue-action--icon), .v2-activity .v2-activity__option { display: inline-flex; align-items: center; justify-content: center; height: 32px; padding: 0 12px; border-radius: 9px; box-shadow: inset 0 0 0 1px var(--d); background: var(--sf); font: 600 13px/1 var(--f-body); color: var(--ink); white-space: nowrap; } +.v2-activity .v2-activity__queue-action--icon { display: inline-grid; place-items: center; width: 32px; height: 32px; border-radius: 9px; background: var(--sf); box-shadow: inset 0 0 0 1px var(--d); color: var(--ink); flex: none; } +.v2-activity .v2-activity__queue-action--icon svg.i { width: 16px; height: 16px; stroke-width: 2; } +.v2-activity .v2-activity__queue-action--secondary.v2-activity__queue-action--icon { background: var(--t); } +/* decision: options are content, one line under the question, first option cobalt */ +.v2-activity .v2-activity__queue-row--decision { row-gap: 12px; } +.v2-activity .v2-activity__queue-row--decision .v2-activity__queue-actions { grid-column: 1 / -1; max-width: none; justify-content: flex-start; align-items: flex-start; flex-wrap: wrap; gap: 8px; } +.v2-activity .v2-activity__option-choice { display: flex; flex-direction: column; gap: 2px; min-width: 0; } +.v2-activity .v2-activity__option--primary { color: var(--on-c); background: linear-gradient(180deg, var(--c-fill-a), var(--c-fill-b)); box-shadow: inset 0 0 0 1px var(--c-fill-b); } +.v2-activity .v2-activity__option-recommended { font: 500 11px/14px var(--f-mono); color: var(--mu); padding-left: 2px; } +.v2-activity .v2-activity__decision-footer { display: contents; } +.v2-activity .v2-activity__option--other { box-shadow: none; background: transparent; color: var(--c-text); padding: 0 6px; } +/* Moved forward */ +.v2-activity .v2-activity__moved-list { display: grid; gap: 10px; } +.v2-activity .v2-activity__moved-group { border-radius: 10px; background: var(--sf); box-shadow: inset 0 0 0 1px var(--d); overflow: hidden; } +.v2-activity .v2-activity__moved-head { display: flex; justify-content: space-between; padding: 9px 12px; border-bottom: 1px solid var(--d); font: 500 11px/16px var(--f-mono); color: var(--se); } +.v2-activity .v2-activity__moved-line { display: grid; grid-template-columns: minmax(100px, auto) minmax(0, 1fr) auto; gap: 8px; padding: 8px 12px; font-size: 13px; line-height: 18px; color: var(--ink2); } +.v2-activity .v2-activity__moved-line + .v2-activity__moved-line { border-top: 1px solid var(--d); } +.v2-activity .v2-activity__moved-line strong { font-weight: 600; color: var(--ink); } +.v2-activity .v2-activity__moved-line span { min-width: 0; overflow-wrap: anywhere; } +.v2-activity .v2-activity__moved-line time { font: 500 11px/18px var(--f-mono); color: var(--mu); white-space: nowrap; } +/* 390: head is the top bar; filters stack; every act 44px; a card's acts are ONE row */ +.app.m .mtop.v2-activity__header { padding: 0 12px 0 16px; gap: 8px; } +.app.m .mtop.v2-activity__header h1 { font-size: 22px; } +.app.m .mtop.v2-activity__header .v2-activity__meta { margin-left: auto; font-size: 11.5px; } +.app.m .v2-activity { gap: 0; } +.app.m .v2-activity .v2-activity__controls { flex-direction: column; align-items: stretch; gap: 8px; margin-bottom: 12px; } +.app.m .v2-activity .v2-activity__window-button { flex: 1 1 0; justify-content: center; height: 36px; } +.app.m .v2-activity .v2-activity__scope-button { height: 36px; } +.app.m .v2-activity .v2-activity__compose-pod { flex: 1; } +.app.m .v2-activity .v2-activity__compose-picker { flex: 1; } +.app.m .v2-activity .v2-activity__compose-picker-button { display: flex; height: 44px; max-width: none; } +.app.m .v2-activity .v2-activity__compose-input { min-height: 40px; } +.app.m .v2-activity .v2-activity__compose-send { height: 44px; padding: 0 20px; font-size: 14px; } +.app.m .v2-activity .v2-activity__start { margin-top: 12px; } +.app.m .v2-activity .v2-activity__start-head { flex-direction: column; align-items: flex-start; gap: 2px; } +.app.m .v2-activity .v2-activity__start-row { grid-template-columns: 28px minmax(0, 1fr); } +.app.m .v2-activity .v2-activity__start-act { grid-column: 1 / -1; } +.app.m .v2-activity .v2-activity__start-act > button { width: 100%; height: 44px; justify-content: center; font-size: 14px; } +.app.m .v2-activity .v2-activity__sections { margin-top: 20px; gap: 22px; } +.app.m .v2-activity .v2-activity__sections--scrolled { margin-top: 2px; } +.app.m .v2-activity .v2-activity__queue-row { grid-template-columns: 28px minmax(0, 1fr); } +.app.m .v2-activity .v2-activity__queue-actions { grid-column: 1 / -1; max-width: none; flex-wrap: nowrap; gap: 8px; } +.app.m .v2-activity .v2-activity__queue-actions > button:not(.v2-activity__queue-action--icon) { flex: 1 1 0; height: 44px; font-size: 14px; } +.app.m .v2-activity .v2-activity__queue-action--icon { width: 44px; height: 44px; } +.app.m .v2-activity .v2-activity__queue-row--decision .v2-activity__queue-actions { flex-wrap: wrap; } +.app.m .v2-activity .v2-activity__queue-row--decision .v2-activity__option-choice { flex: 1 1 100%; } +.app.m .v2-activity .v2-activity__queue-row--decision .v2-activity__option-choice .v2-activity__option { height: 44px; font-size: 14px; } +.app.m .v2-activity .v2-activity__queue-row--decision .v2-activity__option--other { flex: 1 1 0; height: 44px; justify-content: flex-start; } +.app.m .v2-activity .v2-activity__moved-line { grid-template-columns: minmax(0, 1fr) auto; } +.app.m .v2-activity .v2-activity__moved-line span { grid-column: 1 / -1; grid-row: 2; } +/* activity:end */ /* first run: the live v2-first-run__* and v2-chat__start* names. The dialog sits inside .app, over the pod. */ .v2-chat__starts { display: grid; grid-template-columns: minmax(0, max-content); gap: 6px; margin: 2px 0 6px 48px; } .v2-chat__start { all: unset; box-sizing: border-box; cursor: pointer; display: inline-flex; align-items: center; min-height: 32px; padding: 6px 12px; border-radius: 10px; font: 500 13px/1.35 var(--f-body); color: var(--ink); background: linear-gradient(180deg, var(--key-a), var(--key-b)); box-shadow: inset 0 0 0 1px var(--key-edge), var(--sh-key); } @@ -896,6 +1018,10 @@ + + + + @@ -948,7 +1074,7 @@

Same look. Less said.

  • One screen firstRepeated blocks become a table; rare settings collapse.
  • @@ -1402,6 +1528,30 @@

    Same look. Less said.

    + + + +
    +

    Activity

    /v2/activity
    +
    +
    Day zero · 1440one pod, only Scout

    Activity

    0 need you · 0 moved
    Post in
    Write a message. Mention an agent to wake it…
    ⌘↵ to send

    Get started

    2 steps · until your first ask arrives
    Say something to it

    Say hello in the pod where it lives. If it doesn't answer, @mention it.

    Connect a channel

    Slack or Telegram — the agent answers where your team already talks.

    Needs you

    Nothing needs you.

    Moved forward

    Nothing yet today.
    +
    Busy · 14404 need you, 2 pods moved

    Activity

    4 need you · 5 moved
    Post in
    Write a message. Mention an agent to wake it…
    ⌘↵ to send

    Needs you

    4
    Northwind · 12m
    Which launch date goes in the customer email?
    Recommended
    Northwind · 40m
    Send the October invoices to 14 customers
    Field notes · 1h
    Fern
    Northwind · 2h
    Juno Park

    Moved forward

    Northwind3
    IvyReconciled the October ledger; two refunds flagged for review.
    SageDrafted the launch email in two versions.
    AlexMoved “Pricing page copy” to Done.
    Field notes2
    FernSummarized six customer interviews into one doc.
    Juno ParkAdded notes from the Friday call.
    +
    +
    +
    Day zero · 3907 days selected

    Activity

    0 need you · 0 moved
    Post in
    Write a message. Mention an agent to wake it…
    ⌘↵ to send

    Get started

    2 steps · until your first ask arrives
    Say something to it

    Say hello in the pod where it lives. If it doesn't answer, @mention it.

    Connect a channel

    Slack or Telegram — the agent answers where your team already talks.

    Needs you

    Nothing needs you.

    Moved forward

    Nothing moved in the last 7 days.
    Pods
    Needs you
    Team
    Settings
    +
    Busy · 390scrolled to Needs you

    Activity

    4 need you · 5 moved

    Needs you

    4
    Northwind · 12m
    Which launch date goes in the customer email?
    Recommended
    Northwind · 40m
    Send the October invoices to 14 customers
    Field notes · 1h
    Fern
    Northwind · 2h
    Juno Park

    Moved forward

    Northwind3
    IvyReconciled the October ledger; two refunds flagged for review.
    SageDrafted the launch email in two versions.
    AlexMoved “Pricing page copy” to Done.
    Field notes2
    FernSummarized six customer interviews into one doc.
    Juno ParkAdded notes from the Friday call.
    Pods
    4Needs you
    Team
    Settings
    +
    • Class names are the page's own (v2-activity__*), so the build copies them. The card marks redraw KindGlyph and the square keys ActGlyph; the rail and avatars stand in for the shell.
    • Day zero is one card above two quiet sections. Get started is the only filled key on the page (the current step); Needs you and Moved forward stay as dashed one-liners, never a second card.
    • Get started never pops in. Its steps wait on a second fetch (/api/registry/pods/:id/agents) and the connector count, so before this build it landed a beat after the composer and pushed Needs you down. While those are in flight on a day-zero account, the card renders as a placeholder: real head, two step rows of bars, the height of the two-step day-zero card (Scout seated, no connector). An account with one or three steps left shifts by one row when they land, and the card drops if the steps come back empty.
    • At 390 a card's acts are one row. Reply (or Approve) flexes to fill it and the 44px ✓ and ↗ squares sit on its right. Before this build the ≤760 rule .v2-activity__queue-actions > button { flex: 1 1 100% } stacked them, about 60px a card. Decision options may still stack full width; Other… and ↗ share the last row.
    • Moved forward names its window when empty. Today keeps Nothing yet today.; 7 days reads Nothing moved in the last 7 days. (new key, e.g. activity.movedForward.emptyWeek).

    Each state, one line

    +
    Day zero · TodayDrawn. One pod, only Scout: 2 steps, current key Open My Workspace, both sections empty.
    Step copy"Say something to it" no longer claims an @mention is needed: Scout answers a plain hello in My Workspace (walk 11-02). The line now reads "Say hello in the pod where it lives. If it doesn't answer, @mention it."
    Day zero · 7 daysDrawn at 390. Moved forward reads "Nothing moved in the last 7 days."
    Get started · checkingDrawn beside the loaded card at the same height. Reserved only when hasEverHadAttention === false, the queue is 0 and the scope is All pods.
    BusyDrawn. One decision (Recommended under the first option), one approval, two mentions, two pod groups.
    No agent yet (step 1)Not drawn. The composer stays hidden and Hire an agent is the current key; same card, three rows.
    Reply open, ruled, Other…Not drawn. The editor takes the full card width, as prod does.
    Loading, error, count unavailableNot drawn. They keep prod's spinner, __error and Retry.
    +
    +
    +
    Day zero · 390, scrolled7 days, both empty

    Activity

    0 need you · 0 moved

    Get started

    2 steps · until your first ask arrives
    Say something to it

    Say hello in the pod where it lives. If it doesn't answer, @mention it.

    Connect a channel

    Slack or Telegram — the agent answers where your team already talks.

    Needs you

    Nothing needs you.

    Moved forward

    Nothing moved in the last 7 days.
    Pods
    Needs you
    Team
    Settings
    +
    +
    Get started · checkingfirst paint, steps in flight

    Get started

    checking…
    +
    Get started · loadedsame height, nothing moves

    Get started

    2 steps · until your first ask arrives
    Say something to it

    Say hello in the pod where it lives. If it doesn't answer, @mention it.

    Connect a channel

    Slack or Telegram — the agent answers where your team already talks.

    +
    +
    +
    +

    First run

    /v2/pods/:id · Scout welcome + Guide dialog
    helper words welcome – → 16dialog – → 68scroll 390 – → 1.0×
    diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index a55c842db8..71c4a5dd41 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -325,6 +325,7 @@ "title": "Moved forward", "description": "grouped by pod · newest first", "empty": "Nothing yet today.", + "emptyWeek": "Nothing moved in the last 7 days.", "more": "{{count}} more in {{pod}}", "omitted": "{{count}} more updates in {{pod}} not shown", "showLess": "Show less", @@ -335,6 +336,7 @@ "title": "Get started", "kicker_one": "1 step · until your first ask arrives", "kicker_other": "{{count}} steps · until your first ask arrives", + "checking": "checking…", "hire": { "title": "Hire your first agent", "description": "Pick a persona. It gets a seat in this workspace and answers when you @mention it.", @@ -342,7 +344,7 @@ }, "speak": { "title": "Say something to it", - "description": "@mention it in the pod where it lives. Your first message there is the moment this becomes yours.", + "description": "Say hello in the pod where it lives. If it doesn't answer, @mention it.", "cta": "Open {{pod}}", "anyPod": "a pod" }, diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index 568ea1ccb6..33f6ba9dbd 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -325,6 +325,7 @@ "title": "推进的工作", "description": "按 Pod 分组 · 最新在前", "empty": "今天还没有更新。", + "emptyWeek": "最近7天没有新进展。", "more": "{{pod}} 还有{{count}}项", "omitted": "{{pod}} 还有{{count}}项未显示", "showLess": "收起", @@ -335,6 +336,7 @@ "title": "开始使用", "kicker_one": "还有1步 · 直到你的第一个请求到来", "kicker_other": "还有{{count}}步 · 直到你的第一个请求到来", + "checking": "正在检查…", "hire": { "title": "雇用你的第一个智能体", "description": "选一个角色。它会在这个工作空间有一个席位,你 @提及它就会回复。", @@ -342,7 +344,7 @@ }, "speak": { "title": "对它说点什么", - "description": "在它所在的 Pod 中 @ 它。你在那里发出的第一条消息,就是它真正属于你的时刻。", + "description": "在它所在的 Pod 里打个招呼。如果它没回应,就 @提及它。", "cta": "打开 {{pod}}", "anyPod": "Pod" }, diff --git a/frontend/src/v2/__tests__/V2ActivityPage.test.tsx b/frontend/src/v2/__tests__/V2ActivityPage.test.tsx index 879e03bb45..981a6bf0c1 100644 --- a/frontend/src/v2/__tests__/V2ActivityPage.test.tsx +++ b/frontend/src/v2/__tests__/V2ActivityPage.test.tsx @@ -385,7 +385,82 @@ describe('V2ActivityPage', () => { mockGet.mockImplementation(mockFor([silentSeat], [{ status: 'active' }], true)); renderPage(); expect(await screen.findByText('Nothing needs you.')).toBeInTheDocument(); + // The card holds its place while the roster is in flight (d215), then drops when no step is left. + await waitFor(() => expect(screen.queryByRole('heading', { name: 'Get started' })).not.toBeInTheDocument()); + }); + + test('day zero: Get started holds its place while the roster and connectors are in flight, then becomes the card (d215 s-activity)', async () => { + const empty = { ...recap, hasEverHadAttention: false, needsYou: [], agents: [], board: [] }; + const seat = { name: 'scout', displayName: 'Scout' }; + const deferred = () => { + let release: (value: unknown) => void = () => {}; + const promise = new Promise((resolve) => { release = resolve; }); + return { promise, release }; + }; + const mockWith = (registry: Promise, connectors: Promise, recapData = empty) => (url: string) => { + if (url === '/api/activity/decision-queue') return Promise.resolve({ data: { items: [], count: 0, countsByPod: {} } }); + if (url === '/api/integrations/user/all') return connectors; + if (url.startsWith('/api/registry/pods/')) return registry; + return Promise.resolve({ data: recapData }); + }; + + // In flight: the real head with "checking…", two rows of bars, aria-busy, no step act yet. + const roster = deferred(); + const connectors = deferred(); + mockGet.mockImplementation(mockWith(roster.promise, connectors.promise)); + const first = renderPage(); + const pending = await screen.findByTestId('activity-start-pending'); + expect(pending).toHaveAttribute('aria-busy', 'true'); + expect(within(pending).getByRole('heading', { name: 'Get started' })).toBeInTheDocument(); + expect(within(pending).getByText('checking…')).toBeInTheDocument(); + expect(pending.querySelectorAll('.v2-activity__start-row')).toHaveLength(2); + expect(within(pending).queryByRole('button')).not.toBeInTheDocument(); + // It sits where the card will sit: above Needs you. + expect(pending.compareDocumentPosition(screen.getByRole('heading', { name: 'Needs you' })) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + // The roster alone is not enough — the connector count still decides whether step 3 is a row. + await act(async () => { roster.release({ data: { agents: [seat], callerSpoke: false } }); }); + expect(screen.getByTestId('activity-start-pending')).toBeInTheDocument(); + await act(async () => { connectors.release({ data: [] }); }); + await waitFor(() => expect(screen.queryByTestId('activity-start-pending')).not.toBeInTheDocument()); + const card = screen.getByRole('heading', { name: 'Get started' }).closest('section') as HTMLElement; + expect(card).not.toHaveAttribute('aria-busy'); + expect(within(card).getByText('2 steps · until your first ask arrives')).toBeInTheDocument(); + expect(within(card).getByText("Say hello in the pod where it lives. If it doesn't answer, @mention it.")).toBeInTheDocument(); + first.unmount(); + + // Steps come back empty: the placeholder drops and no card replaces it. + const roster2 = deferred(); + mockGet.mockImplementation(mockWith(roster2.promise, Promise.resolve({ data: [{ status: 'active' }] }))); + const second = renderPage(); + expect(await screen.findByTestId('activity-start-pending')).toBeInTheDocument(); + await act(async () => { roster2.release({ data: { agents: [seat], callerSpoke: true } }); }); + await waitFor(() => expect(screen.queryByTestId('activity-start-pending')).not.toBeInTheDocument()); expect(screen.queryByRole('heading', { name: 'Get started' })).not.toBeInTheDocument(); + second.unmount(); + + // Fail-closed: an account that has had an ask, or whose history is unknown, never reserves the slot. + for (const hasEverHadAttention of [true, null]) { + const never = deferred(); + mockGet.mockImplementation(mockWith(never.promise, never.promise, { ...empty, hasEverHadAttention })); + const view = renderPage(); + expect(await screen.findByText('Nothing needs you.')).toBeInTheDocument(); + expect(screen.queryByTestId('activity-start-pending')).not.toBeInTheDocument(); + expect(screen.queryByRole('heading', { name: 'Get started' })).not.toBeInTheDocument(); + view.unmount(); + } + }); + + test('Moved forward names its window when empty (d215 s-activity)', async () => { + mockGet.mockImplementation((url: string) => Promise.resolve({ data: url === '/api/activity/decision-queue' + ? { items: [], count: 0, countsByPod: {} } : { ...recap, needsYou: [], agents: [], board: [] } })); + renderPage(); + expect(await screen.findByText('Nothing yet today.')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: '7 days' })); + expect(await screen.findByText('Nothing moved in the last 7 days.')).toBeInTheDocument(); + expect(screen.queryByText('Nothing yet today.')).not.toBeInTheDocument(); + + await act(async () => { await i18n.changeLanguage('zh-CN'); }); + expect(await screen.findByText('最近7天没有新进展。')).toBeInTheDocument(); }); test('day zero: a roster without the field, or a roster that fails, never reads as spoken (#1648)', async () => { diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 6a1bf9df1e..cdb59a53b6 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -3018,10 +3018,10 @@ describe('v2 layout invariants (CSS rule presence)', () => { const footer44 = '.v2-root .v2-activity__queue-row.v2-activity__queue-row--decision .v2-activity__decision-footer > button.v2-activity__queue-action--icon { flex: none; width: 44px; min-width: 44px; }'; expect(enclosingMedia(icon44)).toBe('@media (max-width: 760px)'); expect(enclosingMedia(footer44)).toBe('@media (max-width: 760px)'); - // And the footer override follows the full-width rule it beats, inside that same block. - const fullWidth = '.v2-activity__decision-footer > button { flex: 1 1 100%;'; - expect(v2.indexOf(footer44)).toBeGreaterThan(v2.indexOf(fullWidth)); - expect(enclosingMedia(fullWidth)).toBe('@media (max-width: 760px)'); + // And the footer override follows the text-button rule it beats, inside that same block. + const footerText = '.v2-activity__decision-footer > button { flex: 1 1 auto;'; + expect(v2.indexOf(footer44)).toBeGreaterThan(v2.indexOf(footerText)); + expect(enclosingMedia(footerText)).toBe('@media (max-width: 760px)'); // Other… is cobalt text; the handoff act is ink (no bordered modifier). expect(ruleBody(v2, '.v2-root .v2-activity__queue-actions button.v2-activity__option--other')).toContain('color: var(--v2-accent-text)'); expect(activityPage).toContain('
    )} - {queueCount === 0 && recap.hasEverHadAttention === false && startSteps.length > 0 && ( + {startPending && ( +
    +
    +

    {t('activity.getStarted.title')}

    + {t('activity.getStarted.checking')} +
    + {/* A step's description is one line at desktop and two on a phone; the second bar shows only ≤760. */} + {['first', 'second'].map((row) => ( + + ))} +
    + )} + {!startPending && startEligible && startSteps.length > 0 && (

    {t('activity.getStarted.title')}

    @@ -1469,7 +1495,7 @@ const V2ActivityPage: React.FC = () => {

    {t('activity.movedForward.title')}

    {t('activity.movedForward.description')}
    - {movedGroups.length === 0 ?
    {t('activity.movedForward.empty')}
    :
    + {movedGroups.length === 0 ?
    {t(window === '7d' ? 'activity.movedForward.emptyWeek' : 'activity.movedForward.empty')}
    :
    {movedGroups.map((group) => { const visibleCount = movedVisibleCounts[group.id] || 3; const visible = group.lines.slice(0, Math.min(visibleCount, group.lines.length)); diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index cf60867012..3b8228f601 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -5582,13 +5582,16 @@ body.modern-ui.v2-canvas { same selectors as the desktop rules above so this block wins by order, not by luck. */ .v2-activity__queue-row { grid-template-columns: 28px minmax(0, 1fr); } .v2-activity__queue-row .v2-activity__queue-actions { grid-column: 1 / -1; grid-row: auto; max-width: none; justify-content: flex-start; flex-wrap: wrap; } - .v2-root .v2-activity__queue-actions > button { flex: 1 1 100%; min-height: 44px; } + /* One action row per card (d215 s-activity, walk 11-02): the text act (Reply, Approve, Mark + handled) grows to fill the row and the 44px icon squares below sit beside it on the right. */ + .v2-root .v2-activity__queue-actions > button { flex: 1 1 auto; min-height: 44px; } /* The reply editor's nested Send only grows in HEIGHT (sprint-review 66493/66498): a descendant rule here gave it flex-basis 100% and it ate the textarea (489px Send beside a 139px input). */ .v2-root .v2-activity__reply > button { min-height: 44px; } .v2-activity__queue-row.v2-activity__queue-row--decision .v2-activity__option-choice { flex: 1 1 100%; max-width: none; } .v2-activity__queue-row.v2-activity__queue-row--decision .v2-activity__option-choice button { width: 100%; min-height: 44px; } - .v2-activity__queue-row.v2-activity__queue-row--decision .v2-activity__decision-footer > button { flex: 1 1 100%; min-height: 44px; } + /* Other… grows and the ↗ handoff square sits beside it: one footer row, as d215 s-activity draws it. */ + .v2-activity__queue-row.v2-activity__queue-row--decision .v2-activity__decision-footer > button { flex: 1 1 auto; min-height: 44px; } /* Icon acts stay 44px squares under 760 — the full-width `> button` rule above would stretch them. */ .v2-root .v2-activity__queue-actions button.v2-activity__queue-action--icon { width: 44px; min-width: 44px; flex: none; } .v2-root .v2-activity__queue-row.v2-activity__queue-row--decision .v2-activity__decision-footer > button.v2-activity__queue-action--icon { flex: none; width: 44px; min-width: 44px; } @@ -5616,6 +5619,28 @@ body.modern-ui.v2-canvas { .v2-activity__start-act { grid-column: 1 / -1; } .v2-root .v2-activity__start-act button { width: 100%; min-height: 44px; } } +/* Get started, checking (d215 s-activity): while the roster and connector count are in flight the + card holds its place — real head, two step rows of bars on the same line boxes as the loaded + rows (20px lines, 32px key; 44px full width at ≤760) — so nothing below it moves. */ +.v2-activity__start--pending .v2-activity__start-num--pending { background: var(--v2-surface-hover); } +.v2-activity__start-bar { display: block; position: relative; height: 20px; } +.v2-activity__start-bar::after { content: ""; position: absolute; left: 0; top: 5px; width: 92%; height: 10px; border-radius: 5px; background: var(--v2-surface-hover); } +.v2-activity__start-bar--title + .v2-activity__start-bar { margin-top: 2px; } +.v2-activity__start-bar--title::after { width: 34%; background: var(--v2-border); } +.v2-activity__start-bar--phone { display: none; } +.v2-activity__start-bar--phone::after { width: 64%; } +.v2-activity__start-bar--key { width: 132px; height: 32px; } +.v2-activity__start-bar--key::after { top: 0; width: 100%; height: 100%; border-radius: var(--v2-radius-sm); background: var(--v2-surface); box-shadow: inset 0 0 0 1px var(--v2-border); } +@media (max-width: 760px) { + .v2-activity__start-bar--phone { display: block; } + .v2-activity__start-bar--key { width: 100%; height: 44px; } +} +@media (prefers-reduced-motion: no-preference) { + .v2-activity__start--pending .v2-activity__start-bar::after { animation: v2-activity-start-pulse 1.4s ease-in-out infinite; } +} +@keyframes v2-activity-start-pulse { + 50% { opacity: 0.55; } +} /* --------------------------------------------------------------------------- Artifacts — direction C, PR 5 (ScaleArtifacts.dc.html). Every file and page shared in your pods. Display head + muted meta; two