diff --git a/README.md b/README.md
index de9c5cb..3fd0e1a 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,12 @@ each dedicated workspace; its bottom avatar manages connected accounts.
The interface is in English. User-entered text and external posts retain their
original language.
+Home uses a Tiimo-inspired day flow with expandable activities and direct
+controls for routine steps, tasks, replies and weight entries. Messages uses
+a Beeper-inspired conversation layout with visible source messages, editable
+drafts and a contextual side panel. These execution surfaces remain mocks;
+their records share browser-session state and external sending is simulated.
+
## Workspaces and current status
| Workspace | Route | Current implementation |
diff --git a/docs/execution-support.md b/docs/execution-support.md
index 020a402..38b2804 100644
--- a/docs/execution-support.md
+++ b/docs/execution-support.md
@@ -36,13 +36,30 @@ the new communication and journal prototypes do not consume those accounts.
## Daily home
-`/` presents a greeting, a large input, and a flexible plan grouped as
+`/` presents a greeting, a large input, and a Tiimo-inspired flexible day flow grouped as
Start here, Routines, and If there’s room. There are no fixed time slots.
Tasks can be reordered, completed, deferred, restored, or followed as routine
steps. Communications can be reviewed directly in a home dialog; changes also
appear in `/support`. Journal entries and corrections appear on the home, and
reader samples open for reading. Research remains a separate work surface.
+Activity blocks expand in place into controls suited to their content: routine
+steps, a task's next action and resumption note, or a weight input that records
+an entry in Journal. Completed and deferred activities stay in their original positions with visible
+status and undo controls. Measurements retain the recorded value in place, and
+reply previews retain their outcome, so the user need not remember what was done. Expanded activity selection
+uses the `activity` URL parameter. This remains a session-only mock; a reload
+restores navigation, not recorded activity data.
+
+Messages uses a Beeper-inspired person list and visible original conversation,
+with reply drafting and review next to source-backed context. On narrow screens
+the list and conversation become separate views. Inbox, set-aside, waiting and
+archive filters describe communication state; a simulated reply does not prove
+that a separate promise was fulfilled. References include the saved
+[Beeper Copilot captures](references/beeper-copilot/README.md),
+[Beeper Desktop](https://blog.beeper.com/2025/02/24/try-out-the-new-beeper-desktop-and-ios-beta/),
+and [open-loop research](gbrain-open-loops-research.md).
+
The input currently uses a deterministic English/Japanese mock interpreter, not an LLM.
It proposes typed actions for task creation/status/reordering, routine steps,
contact reply/review or deferral, notes/weight/medication records, and navigation.
diff --git a/e2e/integrations/home.test.ts b/e2e/integrations/home.test.ts
index 51b3803..d82162f 100644
--- a/e2e/integrations/home.test.ts
+++ b/e2e/integrations/home.test.ts
@@ -1,5 +1,44 @@
import { expect, test } from "../fixtures";
+test("executes routine steps and records a measurement in the shared journal", async ({
+ page,
+ a11y,
+}, testInfo) => {
+ await page.goto("/");
+ await page.waitForLoadState("networkidle");
+ await page.getByRole("button", { name: /Morning routine 0 of 3/ }).click();
+ await expect(page).toHaveURL(/activity=task/);
+ await page.getByRole("checkbox", { name: /Drink water/ }).check();
+ await expect(page.getByRole("button", { name: "Finish routine" })).toBeDisabled();
+ await page.getByRole("checkbox", { name: /Get ready/ }).check();
+ await page.getByRole("checkbox", { name: /Have breakfast/ }).check();
+ expect((await a11y().analyze()).violations).toEqual([]);
+ await page.screenshot({ path: testInfo.outputPath("home-routine.png"), fullPage: true });
+ await page.getByRole("button", { name: "Finish routine" }).click();
+ await page.getByRole("button", { name: /Record weight A quick check-in/ }).click();
+ await page.getByRole("spinbutton", { name: "Weight", exact: true }).fill("65.2");
+ await page.getByRole("button", { name: "Save measurement" }).click();
+ await expect(
+ page.getByRole("button", { name: /Record weight Recorded · 65.2 kg/ }),
+ ).toBeVisible();
+ await page.getByRole("link", { name: "Journal", exact: true }).first().click();
+ await expect(page.getByRole("article", { name: "Weight entry" })).toContainText("65.2 kg");
+ await page.getByRole("link", { name: "Home", exact: true }).first().click();
+ await expect(page.getByRole("button", { name: "Restore Morning routine" })).toBeVisible();
+});
+
+test("shows the day flow and conversation context at desktop width", async ({ page }, testInfo) => {
+ await page.setViewportSize({ width: 1440, height: 1080 });
+ await page.goto("/");
+ await page.waitForLoadState("networkidle");
+ await expect(page.getByRole("region", { name: "Today’s plan" })).toBeVisible();
+ await page.screenshot({ path: testInfo.outputPath("home-desktop.png") });
+ await page.goto("/support?conversation=design-review");
+ await page.waitForLoadState("networkidle");
+ await expect(page.getByRole("textbox", { name: "Reply", exact: true })).toBeVisible();
+ await page.screenshot({ path: testInfo.outputPath("messages-desktop.png") });
+});
+
test("previews a natural-language memo and shares the executed record with journal", async ({
page,
}) => {
@@ -20,7 +59,7 @@ test("previews a natural-language memo and shares the executed record with journ
test("keeps the home usable on narrow screens", async ({ page, a11y }, testInfo) => {
await page.goto("/");
await page.waitForLoadState("networkidle");
- for (const width of [320, 375, 768]) {
+ for (const width of [320, 375, 414, 768]) {
await page.setViewportSize({ width, height: 850 });
await expect(page.getByRole("textbox", { name: "Home input" })).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBe(width);
@@ -47,11 +86,9 @@ test("adds tasks only after confirmation and can defer and restore them", async
await expect(complete).toBeVisible();
await page.getByRole("button", { name: "Apply", exact: true }).click();
await expect(complete).toHaveCount(0);
- await page.locator(".home-rest summary").click();
await page.getByRole("button", { name: "Restore grocery shopping", exact: true }).click();
await complete.click();
await expect(complete).toHaveCount(0);
- await page.locator(".home-rest summary").click();
await expect(
page.getByRole("button", { name: "Restore grocery shopping", exact: true }),
).toBeVisible();
@@ -73,13 +110,13 @@ test("resolves communication within home and carries the status to the shared qu
await expect(dialog).toHaveCount(0);
await expect(
page.getByRole("button", { name: "Open conversation with Haruka Aoki" }),
- ).toHaveCount(0);
+ ).toContainText("Set aside");
await page.getByRole("link", { name: "Messages", exact: true }).first().click();
const rows = page.locator(".support-row");
await expect(rows.filter({ hasText: "Schedule next week’s design review" })).toHaveCount(0);
- await page.getByRole("button", { name: "Later", exact: true }).click();
+ await page.getByRole("button", { name: "Set aside", exact: true }).click();
await rows.filter({ hasText: "Schedule next week’s design review" }).click();
- await page.getByRole("button", { name: "Move to Now" }).click();
+ await page.getByRole("button", { name: "Move to Inbox" }).click();
const back = page.getByRole("button", { name: "Back to messages" });
if (await back.isVisible()) await back.click();
await page.getByRole("link", { name: "Home", exact: true }).first().click();
@@ -95,7 +132,7 @@ test("updates the prose brief as priorities change and opens the referenced conv
await page.waitForLoadState("networkidle");
const brief = page.getByRole("region", { name: "Daily brief" });
await expect(brief).toContainText("Review the documents");
- await expect(brief).toContainText("4 conversations");
+ await expect(brief).toContainText("3 conversations");
await page.getByRole("button", { name: "Complete Review the documents", exact: true }).click();
await expect(brief).not.toContainText("Review the documents");
await brief.getByRole("button", { name: "Reply to Haruka Aoki" }).click();
@@ -104,10 +141,53 @@ test("updates the prose brief as priorities change and opens the referenced conv
exact: true,
});
await dialog.getByRole("button", { name: "Leave for later" }).click();
- await expect(brief).toContainText("3 conversations");
+ await expect(brief).toContainText("2 conversations");
await expect(brief).not.toContainText("Haruka Aoki");
await page.getByRole("textbox", { name: "Home input" }).fill("再開地点:資料の2ページ目から");
await page.getByRole("button", { name: "Preview action" }).click();
await page.getByRole("button", { name: "Apply", exact: true }).click();
await expect(brief).toContainText("資料の2ページ目から");
});
+
+test("keeps completed actions in their original positions with visible outcomes", async ({
+ page,
+ a11y,
+}, testInfo) => {
+ await page.goto("/");
+ await page.waitForLoadState("networkidle");
+ const titles = page.locator(".execution-flow-title");
+ const before = await titles.allTextContents();
+ await page.getByRole("button", { name: "Complete Review the documents", exact: true }).click();
+ await expect(
+ page.getByRole("button", { name: "Restore Review the documents", exact: true }),
+ ).toHaveAttribute("aria-pressed", "true");
+ await expect(
+ page.getByRole("button", { name: "Review the documents Completed", exact: true }),
+ ).toBeVisible();
+ await page.getByRole("button", { name: "Open conversation with Haruka Aoki" }).click();
+ const conversation = page.getByRole("dialog", {
+ name: "Conversation with Haruka Aoki",
+ exact: true,
+ });
+ await conversation.getByRole("button", { name: "Review reply", exact: true }).click();
+ await page
+ .getByRole("dialog", { name: "Review reply", exact: true })
+ .getByRole("button", { name: "Try sending" })
+ .click();
+ await expect(
+ page.getByRole("button", { name: "Open conversation with Haruka Aoki" }),
+ ).toContainText("Reply recorded");
+ await page.getByRole("button", { name: /Record weight A quick check-in/ }).click();
+ await page.getByRole("spinbutton", { name: "Weight", exact: true }).fill("65.2");
+ await page.getByRole("button", { name: "Save measurement" }).click();
+ await expect(
+ page.getByRole("button", { name: /Record weight Recorded · 65.2 kg/ }),
+ ).toBeVisible();
+ expect(await titles.allTextContents()).toEqual(before);
+ expect((await a11y().analyze()).violations).toEqual([]);
+ await page.screenshot({ path: testInfo.outputPath("completion-in-place.png") });
+ await page.getByRole("button", { name: "Restore Review the documents", exact: true }).click();
+ await expect(
+ page.getByRole("button", { name: "Complete Review the documents", exact: true }),
+ ).toHaveAttribute("aria-pressed", "false");
+});
diff --git a/e2e/integrations/support.test.ts b/e2e/integrations/support.test.ts
index 399fa0a..9883db1 100644
--- a/e2e/integrations/support.test.ts
+++ b/e2e/integrations/support.test.ts
@@ -17,6 +17,7 @@ test("reviews an edited reply and keeps the simulated result in history", async
await expect(reply).toHaveValue(/Thank you for sharing/);
const edited = "I reviewed the materials. Let’s discuss the first-time experience.";
await reply.fill(edited);
+ await page.locator(".conversation-scheduling summary:visible").click();
const slot = "Mon, Oct 5, 14:00–14:30";
await page.getByRole("combobox", { name: "Suggested times" }).selectOption(slot);
await page.getByRole("button", { name: "Add to reply" }).click();
@@ -33,7 +34,7 @@ test("reviews an edited reply and keeps the simulated result in history", async
await dialog.getByRole("button", { name: "Try sending" }).click();
await expect(dialog).toHaveCount(0);
await expect(rows.filter({ hasText: reviewSubject })).toHaveCount(0);
- await page.getByRole("button", { name: "History", exact: true }).click();
+ await page.getByRole("button", { name: "Archive", exact: true }).click();
await rows.filter({ hasText: reviewSubject }).click();
await expect(page.locator(".support-outcome")).toContainText(edited);
await expect(page.locator(".support-outcome")).toContainText(slot);
@@ -49,16 +50,16 @@ test("defers a conversation and restores it to the current queue", async ({ page
.fill("土曜の予定を確認してから返事するね。");
await page.getByRole("button", { name: "Leave for later" }).click();
await expect(rows.filter({ hasText: coffeeSubject })).toHaveCount(0);
- await page.getByRole("button", { name: "Later", exact: true }).click();
+ await page.getByRole("button", { name: "Set aside", exact: true }).click();
await rows.filter({ hasText: coffeeSubject }).click();
await expect(page.getByRole("textbox", { name: "Reply", exact: true })).toHaveValue(
"土曜の予定を確認してから返事するね。",
);
- await page.getByRole("button", { name: "Move to Now" }).click();
+ await page.getByRole("button", { name: "Move to Inbox" }).click();
const back = page.getByRole("button", { name: "Back to messages" });
if (await back.isVisible()) await back.click();
await expect(rows.filter({ hasText: coffeeSubject })).toBeVisible();
- await expect(page.getByRole("button", { name: "Now", exact: true })).toHaveAttribute(
+ await expect(page.getByRole("button", { name: "Inbox", exact: true })).toHaveAttribute(
"aria-pressed",
"true",
);
@@ -107,7 +108,7 @@ test("keeps corrections as new records and summarizes only selected current fact
});
test("fits communication and journal interactions in narrow screens", async ({ page, a11y }) => {
- for (const width of [320, 375, 768]) {
+ for (const width of [320, 375, 414, 768]) {
await page.setViewportSize({ width, height: 850 });
await page.goto("/support");
await page.waitForLoadState("networkidle");
diff --git a/src/features/home/daily-brief.tsx b/src/features/home/daily-brief.tsx
index dbf6122..371b839 100644
--- a/src/features/home/daily-brief.tsx
+++ b/src/features/home/daily-brief.tsx
@@ -5,7 +5,11 @@ export function DailyBrief({ onOpenContact }: { onOpenContact: (id: string) => v
const { tasks, statuses, entries } = useWorkspaceState();
const focus = tasks.find((task) => task.section === "focus" && task.status === "todo");
const optional = tasks.find((task) => task.section === "optional" && task.status === "todo");
- const contacts = conversations.filter((person) => (statuses[person.id] ?? "now") === "now");
+ const contacts = conversations.filter(
+ (person) =>
+ (statuses[person.id] ?? "now") === "now" &&
+ (person.replyRequired || statuses[person.id] === "now"),
+ );
const firstContact = contacts[0];
const resume = entries.find(
(entry) =>
diff --git a/src/features/home/execution-workbench.css b/src/features/home/execution-workbench.css
new file mode 100644
index 0000000..8621d66
--- /dev/null
+++ b/src/features/home/execution-workbench.css
@@ -0,0 +1,333 @@
+/* Flexible day flow: quiet activity blocks, with detail where the action happens. */
+.execution-day {
+ min-width: 0;
+}
+.execution-flow-group {
+ margin-bottom: 30px;
+}
+.execution-flow-heading {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+.execution-flow-heading h2 {
+ font-size: 14px;
+ font-weight: 600;
+}
+.execution-flow-heading > span {
+ color: var(--muted-foreground);
+ font-size: 11px;
+}
+.execution-flow-items {
+ display: grid;
+ gap: 9px;
+}
+.execution-flow-card {
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ background: var(--muted);
+ overflow: hidden;
+}
+.execution-flow-card[data-kind="focus"] {
+ background: color-mix(in oklch, var(--color-activity-lilac) 16%, var(--background));
+}
+.execution-flow-card[data-kind="routine"] {
+ background: color-mix(in oklch, var(--color-activity-sage) 16%, var(--background));
+}
+.execution-flow-card[data-kind="reply"] {
+ background: color-mix(in oklch, var(--color-activity-lilac) 9%, var(--background));
+}
+.execution-flow-card[data-kind="weight"] {
+ background: color-mix(in oklch, var(--color-activity-peach) 15%, var(--background));
+}
+.execution-flow-card[data-kind="optional"] {
+ background: color-mix(in oklch, var(--color-activity-sage) 9%, var(--background));
+}
+.execution-flow-card[data-expanded="true"] {
+ border-color: color-mix(in oklch, var(--foreground) 28%, var(--border));
+}
+.execution-flow-card[data-status="done"] {
+ background: var(--background);
+}
+.execution-flow-card[data-status="done"] .execution-flow-title {
+ color: var(--muted-foreground);
+}
+.execution-flow-card[data-status="done"] .execution-complete,
+.execution-flow-card[data-status="done"] .execution-flow-icon {
+ color: var(--color-success);
+}
+.execution-flow-row {
+ display: flex;
+ align-items: center;
+ gap: 13px;
+ padding: 16px 18px;
+}
+.execution-complete,
+.execution-flow-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 0 0 24px;
+ height: 28px;
+ color: var(--muted-foreground);
+}
+.execution-complete {
+ cursor: pointer;
+ border-radius: 50%;
+}
+.execution-complete:disabled {
+ cursor: default;
+ opacity: 0.45;
+}
+.execution-complete:hover:not(:disabled) {
+ color: var(--foreground);
+}
+.execution-complete svg,
+.execution-flow-icon svg {
+ width: 21px;
+ height: 21px;
+ stroke-width: 1.5;
+}
+.execution-flow-open {
+ display: flex;
+ flex: 1;
+ min-width: 0;
+ flex-direction: column;
+ text-align: left;
+ gap: 5px;
+ cursor: pointer;
+ border-radius: 3px;
+}
+.execution-flow-title {
+ font-size: 14px;
+ font-weight: 500;
+ line-height: 1.5;
+}
+.execution-flow-description {
+ color: var(--muted-foreground);
+ font-size: 12px;
+ line-height: 1.5;
+}
+.execution-expand-icon {
+ width: 15px;
+ height: 15px;
+ flex-shrink: 0;
+ color: var(--muted-foreground);
+}
+.execution-flow-card[data-expanded="true"] > .execution-flow-row > .execution-expand-icon {
+ transform: rotate(180deg);
+}
+.execution-inline-detail {
+ padding: 6px 22px 24px 55px;
+}
+.execution-inline-detail h2 {
+ display: none;
+}
+.execution-detail-symbol,
+.execution-eyebrow {
+ display: none;
+}
+.execution-description {
+ font-size: 13px;
+ line-height: 1.8;
+ color: var(--muted-foreground);
+ max-width: 480px;
+}
+.execution-routine-progress {
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ font-size: 11px;
+ color: var(--muted-foreground);
+ margin: 20px 0 8px;
+}
+.execution-step-list {
+ display: grid;
+}
+.execution-step-list label {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 14px 0;
+ cursor: pointer;
+ border-top: 1px solid var(--border);
+ font-size: 13px;
+}
+.execution-step-list label > .execution-step-text {
+ flex: 1;
+}
+.execution-step-list label[data-checked="true"] > .execution-step-text {
+ text-decoration: line-through;
+ color: var(--muted-foreground);
+}
+.execution-step-number {
+ font-size: 11px;
+ color: var(--muted-foreground);
+ font-variant-numeric: tabular-nums;
+}
+.execution-next-step {
+ padding: 14px 0;
+ margin-top: 14px;
+ border-top: 1px solid var(--border);
+}
+.execution-next-step > span {
+ font-size: 11px;
+ color: var(--muted-foreground);
+}
+.execution-next-step p {
+ font-size: 13px;
+ line-height: 1.75;
+ margin-top: 5px;
+}
+.execution-detail-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 20px;
+}
+.execution-resume-note {
+ border-top: 1px solid var(--border);
+ margin-top: 25px;
+ padding-top: 20px;
+}
+.execution-resume-note > label {
+ display: block;
+ font-size: 12px;
+ font-weight: 500;
+ margin-bottom: 10px;
+}
+.execution-resume-note textarea {
+ min-height: 74px;
+ background: var(--background);
+ font-size: 12px;
+}
+.execution-note-footer {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ justify-content: space-between;
+ margin-top: 8px;
+}
+.execution-note-footer > span {
+ color: var(--muted-foreground);
+ font-size: 11px;
+}
+.execution-order {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 6px;
+ margin-top: 20px;
+ color: var(--muted-foreground);
+}
+.execution-order > span {
+ font-size: 11px;
+ margin-right: auto;
+}
+.execution-weight-form {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ gap: 12px;
+ margin-top: 18px;
+}
+.execution-weight-form > label {
+ width: 100%;
+ font-size: 12px;
+}
+.execution-weight-input {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ max-width: 180px;
+}
+.execution-weight-input input {
+ background: var(--background);
+ font-size: 20px;
+ height: 42px;
+ font-variant-numeric: tabular-nums;
+}
+.execution-weight-input > span {
+ color: var(--muted-foreground);
+ font-size: 13px;
+}
+.execution-weight-form > p {
+ width: 100%;
+ color: var(--destructive);
+ font-size: 12px;
+}
+.execution-source-note {
+ font-size: 11px;
+ color: var(--muted-foreground);
+ margin-top: 20px;
+}
+.execution-flow-empty {
+ font-size: 12px;
+ color: var(--muted-foreground);
+ padding: 16px 0;
+}
+.execution-rest {
+ border-top: 1px solid var(--border);
+ padding-top: 20px;
+}
+.execution-rest summary {
+ cursor: pointer;
+ font-size: 12px;
+ color: var(--muted-foreground);
+}
+.execution-rest summary > span {
+ margin-left: 8px;
+ font-variant-numeric: tabular-nums;
+}
+.execution-rest-items {
+ margin-top: 12px;
+}
+.execution-rest-row {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 0;
+ font-size: 13px;
+}
+.execution-rest-row > svg {
+ width: 17px;
+ height: 17px;
+ color: var(--muted-foreground);
+}
+.execution-rest-row > span {
+ flex: 1;
+}
+.execution-rest-row small {
+ display: block;
+ color: var(--muted-foreground);
+ font-size: 11px;
+ margin-top: 3px;
+}
+.execution-flow-open:focus-visible,
+.execution-complete:focus-visible {
+ outline: 2px solid var(--ring);
+ outline-offset: 4px;
+}
+@media (max-width: 767px) {
+ .execution-flow-row {
+ padding: 15px 14px;
+ gap: 10px;
+ }
+ .execution-flow-heading > span {
+ font-size: 10px;
+ }
+ .execution-inline-detail {
+ padding: 4px 18px 20px;
+ }
+ .execution-flow-title {
+ font-size: 13px;
+ }
+ .execution-flow-description {
+ font-size: 11px;
+ }
+ .execution-note-footer {
+ align-items: flex-start;
+ }
+}
diff --git a/src/features/home/execution-workbench.tsx b/src/features/home/execution-workbench.tsx
new file mode 100644
index 0000000..5fff2ce
--- /dev/null
+++ b/src/features/home/execution-workbench.tsx
@@ -0,0 +1,486 @@
+import {
+ ArrowDown,
+ ArrowUp,
+ ArrowUpRight,
+ Check,
+ ChevronDown,
+ Circle,
+ ListChecks,
+ MessageCircle,
+ Pause,
+ RotateCcw,
+ Scale,
+} from "lucide-react";
+import { useState } from "react";
+import { Button } from "#/components/ui/button";
+import { Checkbox } from "#/components/ui/checkbox";
+import { Input } from "#/components/ui/input";
+import { Textarea } from "#/components/ui/textarea";
+import { conversations } from "#/features/support/mock-conversations";
+import {
+ type JournalEntry,
+ type Task,
+ useWorkspaceState,
+} from "#/features/workspace/workspace-state";
+
+type ExecutionWorkbenchProps = {
+ selectedId?: string;
+ onSelect: (id: string) => void;
+ onOpenContact: (id: string) => void;
+};
+
+export function ExecutionWorkbench({
+ selectedId,
+ onSelect,
+ onOpenContact,
+}: ExecutionWorkbenchProps) {
+ const { tasks, setTasks, statuses, entries } = useWorkspaceState();
+ const currentEntries = entries.filter(
+ (entry) => !entries.some((other) => other.correctionOf === entry.id),
+ );
+ const weightEntry = currentEntries.find((entry) => entry.kind === "体重");
+ const replies = conversations.filter(
+ (conversation) => conversation.replyRequired || statuses[conversation.id] === "now",
+ );
+ function toggle(id: string) {
+ onSelect(selectedId === id ? "" : id);
+ }
+ function finish(id: string) {
+ setTasks((current) =>
+ current.map((task) =>
+ task.id === id ? { ...task, status: task.status === "todo" ? "done" : "todo" } : task,
+ ),
+ );
+ }
+ return (
+ A little breathing room.{section.label}
+
+ {task.section === "routine" + ? "Your routine" + : task.section === "optional" + ? "If there’s room" + : "One next step"} +
++ {task.id === "review" + ? "Look through the first-time experience. Leave a short note on anything that feels unclear." + : task.section === "optional" + ? "There’s room to decide later. Start whenever it feels right." + : "Choose a small first step. You can leave a note if you need to pause."} +
+ {task.id === "review" && ( +Open the materials and look at the first screen.
+A quick check-in
+Just a measurement. No target to hit.
+ + {entry && ( +{entry.text} kg
+Your measurement also appears in Journal.
+ > + ); +} diff --git a/src/features/home/home-page.tsx b/src/features/home/home-page.tsx index 6193195..6bdb617 100644 --- a/src/features/home/home-page.tsx +++ b/src/features/home/home-page.tsx @@ -1,21 +1,8 @@ import { Link, useNavigate, useRouteContext } from "@tanstack/react-router"; -import { - ArrowDown, - ArrowUp, - ArrowUpRight, - Check, - ChevronRight, - Circle, - Coffee, - MessagesSquare, - Plus, - RotateCcw, - X, -} from "lucide-react"; +import { ArrowUp, ArrowUpRight, Plus, X } from "lucide-react"; import { useRef, useState } from "react"; import { Dialog } from "#/components/dialog"; import { Button } from "#/components/ui/button"; -import { Checkbox } from "#/components/ui/checkbox"; import { SidebarInset, SidebarProvider } from "#/components/ui/sidebar"; import { Textarea } from "#/components/ui/textarea"; import { TooltipProvider } from "#/components/ui/tooltip"; @@ -26,11 +13,12 @@ import { ConversationDetail } from "#/features/support/conversation-detail"; import { conversations } from "#/features/support/mock-conversations"; import { entryKindLabels, useWorkspaceState } from "#/features/workspace/workspace-state"; import { DailyBrief } from "./daily-brief"; +import { ExecutionWorkbench } from "./execution-workbench"; import { type HomeAction, interpretAction } from "./interpret-action"; export function HomePage() { const { auth } = useRouteContext({ from: "__root__" }); - const { tasks, setTasks, statuses, setStatuses, outcomes, setOutcomes, entries, setEntries } = + const { tasks, setTasks, setStatuses, outcomes, setOutcomes, entries, setEntries } = useWorkspaceState(); const navigate = useNavigate(); const input = useRef{task.title}
- {task.steps && ( -It’s okay to leave some room.
- )} -Select a conversation
+