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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

### Added

- Make Team the default Space view while honoring a saved view choice in `acon.space.section`; the Space header now scrolls with the page. Add per-group attention and active-agent summaries, collapsed idle/away rows, and namespaced actor detail routes with responsive presentation and focus restoration; keep the roster in Room. Extend the browser probe for Team/detail navigation and persistence, including a reload with `view` removed (#101).
- Add optional public Space groups, actor roles and state-change timestamps, with a group filter and Room as the default view. Raise caps to 96 actors and 128 links; save the Flat/Room/3D sub-toggle separately as `acon.space.roomView`. These API 1 fields are additive: N-1 compatibility is unaffected because older consoles ignore unknown optional fields.
- Add synthetic H1/A1 lineage and comparison integration fixtures with explicit owner/project/hash routing, bounded pages and lazy evidence reads, plus isolated compact/wide keyboard and visual checks (#63).
- Serve authenticated MCP principal tools at `/principal/mcp` with a private live registry, verified Access or separate bearer identity, per-principal rate and concurrency limits, read-only filtering, bounded bodies/results and metadata-only audit. Add `acon principal list|off|on` and document the 2025-11-25 transport and registry schema (#88).
Expand Down
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,21 +92,27 @@ Space is an optional area in the rail, shown when a ready plugin declares `capab

The area uses `#/local/space`; if a plugin already owns the tool id `space`, the area's link uses `#/local/~space` so that plugin's existing URLs keep working.

The Space header provides a summary strip, view tabs and a group filter. Routes accept `#/local/space?view=team|flow|board|room&group=<id>`; the selected view is saved as `acon.space.view`. Room remains the default view in this delivery. Team, Flow and Board are opt-in placeholders until their later deliveries. Room contains the existing Flat, Room and, when WebGL2 is available, 3D scene controls; their sub-toggle persists separately as `acon.space.roomView`. Summary state counts filter actors (`state=working|waiting|blocked|away|idle`), and the project count resets both filters. Flat keeps the normal layout. Room uses CSS 3D floor and wall planes with upright actor/card buttons, without downloading Three.js. Rotate left/right changes yaw by 90 degrees; zoom has three steps. Focus the scene and use Left/Right or +/- for the same controls. Reduced motion removes camera transitions.
The Space header provides a summary strip, view tabs and a group filter. Routes accept `#/local/space?view=team|flow|board|room&group=<id>`; Team is the default, while a saved view choice in `acon.space.section` wins when the route omits `view`. The header scrolls with the page. Actor detail uses `&actor=<namespaced-key>`. Flow and Board remain placeholders for their later deliveries. Summary state counts filter actors (`state=working|waiting|blocked|away|idle`), and the project count resets both filters.

Team puts blocked, waiting and approval-holding actors first under Needs attention, then shows each group with agent and active counts and shortcuts to that group's Board and Flow. Working, waiting and blocked actors appear as tiles with their avatar, roles, state, current task, inbox and approval counts. Idle and away actors collapse into one expandable line per group. Team is the text alternative; the separate actor/zone/link roster remains available in Room.

Selecting an actor in Team or Room opens its detail using the namespaced actor route. At 1024 px and wider, detail is a side sheet; below 1024 px, it is a full page. Escape or Back returns to the prior view and restores its scroll position and focus. Detail includes the actor's current task and producer link, marks, and current overlap/conflict/cohort partners. Its Interactions section is a placeholder for the Flow buffer and says that history appears once observed. Open in the actor's tool follows its existing link. Unknown actor routes show a short notice with a next step.

Room contains the existing Flat, Room and, when WebGL2 is available, 3D scene controls; their sub-toggle persists separately as `acon.space.roomView`. Flat keeps the normal layout. Room uses CSS 3D floor and wall planes with upright actor/card buttons, without downloading Three.js. Rotate left/right changes yaw by 90 degrees; zoom has three steps. Focus the scene and use Left/Right or +/- for the same controls. Reduced motion removes camera transitions.

3D is opt-in and loads the vendored renderer only when selected. The default camera frames the scene's content, turning its long axis down the screen on a portrait canvas; pack models are fitted by width with a height cap so desks, boards and shelves read as furniture. Room frames every billboard inside its scroll box. Drag empty canvas space to orbit, or use the same camera controls. Actor and work-item buttons remain in the same keyboard order and follow their projected screen positions; focus or hover reveals their labels. The text roster remains available and leads on phones. Missing WebGL2, renderer startup failure or context loss returns to Room with a notice. Canvas draws happen on changed public facts, asset completion and camera input, with a sprite burst bounded to 0.8 seconds; settled scenes schedule no frame or animation timer. Reduced motion draws the final pose immediately.

The exported `SpaceScene` contract contains:

- Optional `groups: {id, title}[]` describe public groups. Actors, zones and cards may each carry an optional `group` id; the shell namespaces group ids by tool. If a scene omits groups, its content is assigned to one group named after that tool. In a scene with declared groups, items without a group remain visible under every project filter; zones needed by matching cards remain visible even across group boundaries. These fields are additive API 1 metadata and may be ignored by older consoles.
- Optional `groups: {id, title}[]` describe public groups. Actors, zones and cards may each carry an optional `group` id; the shell namespaces group ids by tool. If a scene omits groups, its content is assigned to one group named after that tool. The shell keeps the existing 200-group bound per tool, preserving distinct group ids across tools instead of cutting the combined list into ungrouped content. In a scene with declared groups, items without a group remain visible under every project filter; zones needed by matching cards remain visible even across group boundaries. These fields are additive API 1 metadata and may be ignored by older consoles.
- `actors`: `id`, `name`, `state` (`idle`, `working`, `waiting`, `blocked`, `away`), optional `pet`, `card`, `marks`, `group`, public `roles` (up to 4 role names, 32 characters each) and ISO `since`, plus a panel `link`.
- `marks`: `kind` (`approval`, `inbox`, `paused`, `mode`) and public `text`.
- `zones`: `id`, `title`, optional `kind` (`queue`, `work`, `review`, `done`) and a non-negative `count`.
- `cards`: `id`, public `title`, `zone`, optional `owner` and `tone`, plus a panel `link`.
- Optional `links`: `kind` (`message`, `overlap`, `conflict`, `cohort`), actor ids `from` and `to`, and optional public `label` and ISO `at`.
- Panel links use `{panel, params?}`. Routing parameters must contain public identifiers, never bodies, private paths or task details.

Strings are limited to 200 characters; at most 96 actors, 200 cards and 128 links are rendered across tools. The server copies only allowed fields and reports dropped records; the shell also caps the combined scene. Conformance rejects extra fields, oversized strings and excess records with named problems. All state labels include text and a glyph. Clicking an actor or card navigates to its panel; Space performs no action. A text roster lists the same actors and zone counts and leads on phones, with the visual scene inside its own scroll box.
Strings are limited to 200 characters; at most 96 actors, 200 cards and 128 links are rendered across tools. The server copies only allowed fields and reports dropped records; the shell also caps the combined scene. Conformance rejects extra fields, oversized strings and excess records with named problems. All state labels include text and a glyph. Clicking a card navigates to its panel; clicking an actor opens its detail, whose Open in button navigates to the actor panel; Space performs no action. A text roster lists the same actors and zone counts and leads on phones, with the visual scene inside its own scroll box.

Office packs live at `$AGENTHUB_HOME/console/assets/<id>/`. A `pack.json` is at most 16 KB and has this form:

Expand Down
22 changes: 17 additions & 5 deletions scripts/ui-check-space-foundation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ export async function runSpaceFoundation({ browser, ticket, report, output }) {
const context = await browser.newContext({ viewport: { width, height: 1000 }, reducedMotion: "reduce" });
try {
const page = await context.newPage();
let spacePolls = 0;
page.on("request", (request) => {
if (new URL(request.url()).pathname !== "/p/demo/snapshot" || request.method() !== "POST") return;
try { if (request.postDataJSON()?.panel === "space") spacePolls += 1; } catch { /* Ignore non-JSON requests. */ }
});
await page.goto((await ticket()).url);
await page.evaluate(() => { location.hash = "#/local/space?view=room"; });
await page.locator(".space-actor").nth(5).waitFor();
Expand All @@ -17,24 +22,31 @@ export async function runSpaceFoundation({ browser, ticket, report, output }) {
assert(Math.abs(layout.view - layout.main) < 2, "Space does not fill main width");
assert(layout.document <= layout.viewport + 1, "Space causes page overflow");
await page.locator("[data-space-section-tab='team']").click();
await page.getByText("Team view is not available yet. Room shows the current scene.").waitFor();
assert.equal(await page.evaluate(() => localStorage.getItem("acon.space.view")), "team");
await page.locator(".space-team").waitFor();
assert((await page.locator(".space-team").innerText()).trim().length > 0, "Team view has no content");
assert.equal(await page.evaluate(() => localStorage.getItem("acon.space.section")), "team");
await page.evaluate(() => { location.hash = "#/local/space"; });
await page.reload();
await page.getByText("Team view is not available yet. Room shows the current scene.").waitFor();
await page.locator(".space-team").waitFor();
assert.equal(await page.evaluate(() => new URLSearchParams(location.hash.split("?")[1] ?? "").get("view")), null, "Persistence reload retained an explicit view route");
await page.locator("[data-space-section-tab='room']").click();
await page.locator(".space-actor").nth(5).waitFor();
const pollsBeforeFilters = spacePolls;
await page.locator("#space-project").selectOption("demo:delivery");
await page.waitForFunction(() => location.hash.includes("group=demo%3Adelivery") && document.querySelectorAll(".space-actor").length === 2);
await page.locator(".space-summary button").filter({ hasText: /working$/ }).click();
await page.waitForFunction(() => location.hash.includes("state=working") && document.querySelectorAll(".space-actor").length === 1);
await page.waitForTimeout(150);
assert.equal(spacePolls, pollsBeforeFilters, "Group/state route changes triggered a full Space poll");
const screenshot = `foundation-${width}.png`;
await page.screenshot({ path: join(output, screenshot) });
report.captures.push({ key: `foundation-${width}`, screenshot, width, view: "space-foundation" });
report.spaceFoundation.push({ width, ...layout, routing: true, persistence: true, groupAndStateFilters: true });
report.spaceFoundation.push({ width, ...layout, routing: true, persistence: true, groupAndStateFilters: true, filterChangesWithoutRepoll: true });
} catch (error) { report.findings.push({ kind: "space-foundation", width, detail: error.message }); }
finally { await context.close(); }
}
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 }, reducedMotion: "reduce" });
// The probe polls with waitForFunction, which the shell CSP blocks as eval; bypass it for this measurement context only.
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 }, reducedMotion: "reduce", bypassCSP: true });
try {
const page = await context.newPage(); let calls = 0, change = false;
await page.route("**/p/*/snapshot", async (route) => {
Expand Down
Loading
Loading