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: 3 additions & 3 deletions apps/presentation/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@
"smoke:home-route": "rm -rf /tmp/loopx-home-route-smoke && tsc --ignoreConfig --target ES2022 --module CommonJS --moduleResolution Node --ignoreDeprecations 6.0 --skipLibCheck --strict --outDir /tmp/loopx-home-route-smoke smoke/home-route-smoke.ts && node /tmp/loopx-home-route-smoke/home-route-smoke.js",
"smoke:delegation-preflight": "rm -rf node_modules/.cache/loopx-delegation-preflight-smoke && tsc --ignoreConfig --target ES2022 --module NodeNext --moduleResolution NodeNext --jsx react-jsx --skipLibCheck --strict --rootDir . --outDir node_modules/.cache/loopx-delegation-preflight-smoke smoke/delegation-preflight-smoke.tsx src/data/delegation-preflight.ts src/features/personal-workspace/delegation-preflight-status.tsx && node node_modules/.cache/loopx-delegation-preflight-smoke/smoke/delegation-preflight-smoke.js",
"smoke:delegation-preflight-browser": "node --experimental-strip-types smoke/delegation-preflight-browser-smoke.mjs",
"smoke:personal-workspace": "npm run smoke:conversation-history-order && npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/steering-recovery.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace": "npm run smoke:conversation-history-order && npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/steering-recovery.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/goal-suggestions.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:workspace-locale": "LOOPX_PERSONAL_WORKSPACE_SCENARIO=workspace-locale node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-packaged": "npm run smoke:conversation-history-order && npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/steering-recovery.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser/server-startup.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-evidence-return-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-feedback-recovery-smoke.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/goal-workspace-craft-browser-smoke.mjs",
"smoke:personal-workspace-packaged:catalog": "npm run smoke:conversation-history-order && npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/steering-recovery.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser/server-startup.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-packaged": "npm run smoke:conversation-history-order && npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/steering-recovery.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/goal-suggestions.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser/server-startup.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-evidence-return-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-feedback-recovery-smoke.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/goal-workspace-craft-browser-smoke.mjs",
"smoke:personal-workspace-packaged:catalog": "npm run smoke:conversation-history-order && npm run smoke:goal-order && npm run smoke:goal-activity && npm run smoke:proposal-recency && node src/features/personal-workspace/conversation-order.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/steering-recovery.test.mjs && node --no-warnings --experimental-strip-types --test src/features/personal-workspace/goal-suggestions.test.mjs && npm run smoke:delegation-preflight && node src/features/personal-workspace/workspace-theme.test.mjs && node src/features/personal-workspace/font-token.test.mjs && node src/features/personal-workspace/personal-workspace-contract.test.mjs && node ../../../examples/personal-workspace-browser/server-startup.test.mjs && LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node ../../../examples/personal-workspace-browser-smoke.mjs",
"smoke:personal-workspace-packaged:team": "LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-evidence-return-smoke.mjs && LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/team-feedback-recovery-smoke.mjs",
"smoke:personal-workspace-packaged:craft": "LOOPX_PERSONAL_WORKSPACE_PACKAGED=1 LOOPX_PLAYWRIGHT_PACKAGE=\"$PWD/node_modules/playwright\" node smoke/goal-workspace-craft-browser-smoke.mjs",
"smoke:todo-resume-condition": "rm -rf /tmp/loopx-todo-resume-condition-smoke && tsc --ignoreConfig --target ES2022 --module commonjs --moduleResolution node --ignoreDeprecations 6.0 --skipLibCheck --strict --outDir /tmp/loopx-todo-resume-condition-smoke smoke/todo-resume-condition-smoke.ts src/features/personal-workspace/todo-resume-condition.ts && node /tmp/loopx-todo-resume-condition-smoke/smoke/todo-resume-condition-smoke.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,12 @@
.personal-composer-folded .personal-composer-tools > summary { padding-inline: 8px; border-radius: 6px; list-style: none; }
.personal-composer-folded .personal-composer-tools > summary::-webkit-details-marker { display: none; }
.personal-composer-folded .personal-composer-tools > summary:hover { background: var(--pw-hover); color: var(--pw-text); }
.personal-composer-folded .personal-composer-tools .personal-quick-prompts { position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 20; display: grid; gap: 4px; width: max-content; max-width: min(320px, calc(100vw - 32px)); padding: 6px; border: 1px solid var(--pw-line); border-radius: 12px; background: var(--pw-card); box-shadow: 0 12px 32px rgb(0 0 0 / 8%), 0 1px 2px rgb(0 0 0 / 6%); animation: goal-panel-enter var(--motion-base) var(--ease-out); }
.personal-composer-folded .personal-quick-prompts button { justify-content: flex-start; }
.personal-composer-folded .personal-composer-tools > summary { display: block; max-width: min(60vw, 420px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The folded composer's rows pop up as one panel, so a derived row above the shipped prompts never widens the trigger. */
.personal-composer-folded .personal-composer-tools .personal-composer-tools-panel { position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 20; display: grid; gap: 6px; width: max-content; max-width: min(320px, calc(100vw - 32px)); padding: 6px; border: 1px solid var(--pw-line); border-radius: 12px; background: var(--pw-card); box-shadow: 0 12px 32px rgb(0 0 0 / 8%), 0 1px 2px rgb(0 0 0 / 6%); animation: goal-panel-enter var(--motion-base) var(--ease-out); }
.personal-composer-folded .personal-composer-tools .personal-quick-prompts, .personal-composer-folded .personal-composer-tools .personal-goal-suggestions { display: grid; gap: 4px; margin: 0; }
.personal-composer-folded .personal-composer-tools .personal-goal-suggestions { padding-bottom: 6px; border-bottom: 1px solid var(--pw-line); }
.personal-composer-folded .personal-quick-prompts button, .personal-composer-folded .personal-goal-suggestions button { justify-content: flex-start; }
.personal-composer-pill { display: flex; align-items: center; gap: 10px; flex: 0 1 560px; min-width: 0; min-height: 44px; padding: 0 16px; border: 1px solid var(--pw-line-strong); border-radius: 16px; background: var(--pw-card); color: var(--pw-faint); cursor: text; font: inherit; font-size: var(--text-body); line-height: var(--leading-body); text-align: left; transition: border-color var(--motion-fast) var(--ease-standard), color var(--motion-fast) var(--ease-standard); animation: goal-panel-enter var(--motion-section) var(--ease-out); }
.personal-composer-pill span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.personal-composer-pill:hover { border-color: var(--pw-muted); color: var(--pw-muted); }
Expand All @@ -87,7 +91,7 @@
@keyframes goal-panel-enter { from { opacity: 0; transform: translateY(4px); } }

@media (prefers-reduced-motion: reduce) {
.personal-goal-view-panel:not([hidden]), .personal-composer-pill, .personal-composer-folded .personal-quick-prompts { animation: none; }
.personal-goal-view-panel:not([hidden]), .personal-composer-pill, .personal-composer-folded .personal-composer-tools-panel { animation: none; }
.goal-overview-criteria li, .goal-overview-attention li, .goal-overview-run svg, .goal-overview-attention li svg:last-child { transition: none; }
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import assert from "node:assert/strict";
import test from "node:test";

import { deriveGoalSuggestions, goalSuggestionLimit } from "./goal-suggestions.ts";

const classes = (overrides = {}) => ({ running_observed: 0, needs_you: 0, waiting_dependency: 0, open: 0, blocked: 0, deferred: 0, done: 0, ...overrides });
const digest = ({ activation = "active", open = 0, done = 0, blocked = 0, blockedIds = [], waiting = 0, optional = 0, listed = waiting + optional, oldest = null, needsYouUnavailable = false, coverage = "complete", projectedOpen = open } = {}) => ({
lifecycle: { activation, finishingTurn: false },
work: { open, done, total: coverage === "complete" ? open + done : null, classes: classes({ open, blocked, done }), rows: [], groups: [],
rail: { needsYou: [], open: [], blocked: blockedIds, done: [] }, coverage, listedOpen: open, projectedOpen },
needsYou: needsYouUnavailable ? { availability: "unavailable" } : { availability: "ok", blocking: waiting, optional, coverage, listed, oldestWaitingSince: oldest },
});
const todos = [{ todoId: "t1", text: "Backfill March exports" }, { todoId: "t2", text: " Reconcile totals " }, { todoId: "t3", text: "Third" }];

test("a stopped Goal invites nothing", () => {
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ activation: "stopped", open: 3, blocked: 1, waiting: 2 }), todos }), []);
});

test("decisions waiting on the owner come first and carry their count and age", () => {
const [first] = deriveGoalSuggestions({ digest: digest({ open: 4, blocked: 1, blockedIds: ["t1"], waiting: 2, oldest: "2026-10-15T08:00:00Z" }), todos });
assert.deepEqual(first, { kind: "open_decisions", count: 2, oldestWaitingSince: "2026-10-15T08:00:00Z" });
});

test("blocked work names at most two real task titles from the rail", () => {
const [blocked] = deriveGoalSuggestions({ digest: digest({ open: 3, blocked: 3, blockedIds: ["t2", "missing", "t1", "t3"] }), todos });
assert.deepEqual(blocked, { kind: "explain_blocked", count: 3, titles: ["Reconcile totals", "Backfill March exports"] });
});

test("a failing acceptance criterion is cited by its own description", () => {
const [failing] = deriveGoalSuggestions({ digest: digest({ open: 1 }), todos, criteria: [
{ description: "Totals reconcile", state: "accepted" }, { description: "Backfill matches CSV", state: "failed" }, { description: "Runbook approved", state: "failed" },
] });
assert.deepEqual(failing, { kind: "explain_failed_criterion", criterion: "Backfill matches CSV", failedCount: 2 });
});

test("idle requires an observed idle executor, open work, and nothing waiting on the owner", () => {
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 2 }), todos, execution: { kind: "idle" } }), [{ kind: "continue_idle", open: 2 }]);
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 2 }), todos, execution: { kind: "unknown" } }), [], "unknown is not idle");
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 2 }), todos, execution: { kind: "running" } }), []);
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 2, waiting: 1 }), todos, execution: { kind: "idle" } }).map((row) => row.kind), ["open_decisions"], "the owner is the blocker, not idleness");
});

test("a finished Goal points at its results only when nothing waits or blocks", () => {
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 0, done: 5 }), todos }), [{ kind: "review_results", done: 5 }]);
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 0, done: 5, blocked: 1, blockedIds: ["t1"] }), todos }).map((row) => row.kind), ["explain_blocked"]);
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 0, done: 0 }), todos }), [], "an empty Goal suggests nothing");
});

test("needs-you unavailable or unknown is not zero: no decisions, no idle, no finish", () => {
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 1, needsYouUnavailable: true }), todos, execution: { kind: "idle" } }), [], "unavailable attention cannot prove nothing waits");
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 1, waiting: null, listed: 0 }), todos, execution: { kind: "idle" } }), [], "an unknown blocking count is not zero");
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 0, done: 4, waiting: null, listed: 0 }), todos }), [], "an unknown blocking count never finishes a Goal");
});

test("an optional user action is listed but is not a decision", () => {
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 2, waiting: 0, optional: 1 }), todos, execution: { kind: "idle" } }), [{ kind: "continue_idle", open: 2 }]);
const [first] = deriveGoalSuggestions({ digest: digest({ open: 2, waiting: 2, optional: 3, listed: 1 }), todos });
assert.deepEqual(first, { kind: "open_decisions", count: 2, oldestWaitingSince: null }, "the owner's blocking count, not the listed items, is the decision count");
});

test("a partial listing never claims the Goal is finished and counts projected open work", () => {
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 0, done: 3, coverage: "partial", projectedOpen: 5 }), todos }), [], "zero listed items of five open is not finished");
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 0, done: 3, coverage: "unknown", projectedOpen: null }), todos }), [], "unknown coverage is not finished");
assert.deepEqual(deriveGoalSuggestions({ digest: digest({ open: 1, done: 3, coverage: "partial", projectedOpen: 5 }), todos, execution: { kind: "idle" } }), [{ kind: "continue_idle", open: 5 }], "idle continues the projected open work");
});

test("at most three suggestions, in owner priority order", () => {
const rows = deriveGoalSuggestions({ digest: digest({ open: 2, blocked: 1, blockedIds: ["t1"], waiting: 1 }), todos, execution: { kind: "idle" },
criteria: [{ description: "X", state: "failed" }] });
assert.equal(rows.length, goalSuggestionLimit);
assert.deepEqual(rows.map((row) => row.kind), ["open_decisions", "explain_blocked", "explain_failed_criterion"]);
});
Loading
Loading