portable-agent-layer 0.72.0 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -7
- package/src/cli/index.ts +21 -2
- package/src/cli/server.ts +33 -0
- package/src/hooks/handlers/agenda.ts +195 -7
- package/src/hooks/handlers/update-check.ts +1 -1
- package/src/hooks/lib/agenda-store.ts +2 -0
- package/src/hooks/lib/goal-links.ts +83 -0
- package/src/hooks/lib/models.ts +1 -1
- package/src/hooks/lib/projects.ts +5 -0
- package/src/hooks/lib/settings.ts +2 -0
- package/src/hooks/lib/telos-goals.ts +8 -2
- package/src/hooks/lib/token-usage.ts +2 -0
- package/src/tools/agent/project.ts +22 -20
- package/src/tools/control-room/attention.ts +146 -0
- package/src/tools/control-room/data.ts +76 -5
- package/src/tools/control-room/detail.ts +158 -0
- package/src/tools/control-room/matrix.ts +91 -19
- package/src/tools/control-room/prefs.ts +96 -0
- package/src/tools/control-room/server-config.ts +8 -0
- package/src/tools/control-room/server.ts +196 -11
- package/src/tools/control-room/snooze.ts +65 -0
- package/src/tools/control-room/static.ts +68 -0
- package/src/tools/control-room/ui/app.tsx +196 -50
- package/src/tools/control-room/ui/attention-bell.tsx +118 -0
- package/src/tools/control-room/ui/components/README.md +18 -0
- package/src/tools/control-room/ui/components/badge.tsx +38 -0
- package/src/tools/control-room/ui/components/button.tsx +43 -0
- package/src/tools/control-room/ui/components/card.tsx +45 -0
- package/src/tools/control-room/ui/components/input.tsx +24 -0
- package/src/tools/control-room/ui/components/label.tsx +18 -0
- package/src/tools/control-room/ui/components/popover.tsx +37 -0
- package/src/tools/control-room/ui/components/separator.tsx +25 -0
- package/src/tools/control-room/ui/components/skeleton.tsx +14 -0
- package/src/tools/control-room/ui/components/switch.tsx +27 -0
- package/src/tools/control-room/ui/components/table.tsx +60 -0
- package/src/tools/control-room/ui/components/toggle-group.tsx +38 -0
- package/src/tools/control-room/ui/dist/assets/index-BoQjFpzV.js +49 -0
- package/src/tools/control-room/ui/dist/assets/index-Dncp2bYg.css +2 -0
- package/src/tools/control-room/ui/dist/index.html +19 -0
- package/src/tools/control-room/ui/format.ts +0 -12
- package/src/tools/control-room/ui/frame.tsx +125 -0
- package/src/tools/control-room/ui/index.html +2 -2
- package/src/tools/control-room/ui/lib/api.ts +27 -0
- package/src/tools/control-room/ui/lib/cn.ts +6 -0
- package/src/tools/control-room/ui/lib/write.ts +43 -0
- package/src/tools/control-room/ui/package.json +28 -0
- package/src/tools/control-room/ui/parts.tsx +235 -0
- package/src/tools/control-room/ui/screens/log.tsx +274 -0
- package/src/tools/control-room/ui/screens/project-detail.tsx +302 -0
- package/src/tools/control-room/ui/screens/projects.tsx +128 -0
- package/src/tools/control-room/ui/screens/settings.tsx +205 -0
- package/src/tools/control-room/ui/screens/today.tsx +391 -0
- package/src/tools/control-room/ui/theme.css +127 -0
- package/src/tools/control-room/ui/vite.config.ts +36 -0
- package/src/tools/control-room/writes.ts +106 -0
- package/src/tools/ledger/outcomes.ts +9 -0
- package/src/tools/ledger/query.ts +2 -0
- package/src/tools/ledger/view.ts +31 -10
- package/src/tools/lib/handoff-note.ts +14 -0
- package/src/tools/lib/project-isc.ts +61 -0
- package/src/tools/control-room/ui/agenda.tsx +0 -43
- package/src/tools/control-room/ui/agents.tsx +0 -67
- package/src/tools/control-room/ui/app.css +0 -857
- package/src/tools/control-room/ui/board.tsx +0 -82
- package/src/tools/control-room/ui/handoffs.tsx +0 -37
- package/src/tools/control-room/ui/ledger.tsx +0 -137
- package/src/tools/control-room/ui/matrix.tsx +0 -117
- package/src/tools/control-room/ui/panel.tsx +0 -60
- package/src/tools/control-room/ui/signal.tsx +0 -161
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import type { ControlRoomPrefs } from "../../prefs";
|
|
3
|
+
import type { ServerStatus } from "../../server";
|
|
4
|
+
import { Button } from "../components/button";
|
|
5
|
+
import { Input } from "../components/input";
|
|
6
|
+
import { Label } from "../components/label";
|
|
7
|
+
import { Switch } from "../components/switch";
|
|
8
|
+
import { Empty, Panel, Pending } from "../frame";
|
|
9
|
+
import { useLoaded } from "../lib/api";
|
|
10
|
+
import { setInstallSettings, setPrefs as writePrefs } from "../lib/write";
|
|
11
|
+
|
|
12
|
+
interface InstallSettings {
|
|
13
|
+
actor: string;
|
|
14
|
+
timezone: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function Field({
|
|
18
|
+
label,
|
|
19
|
+
hint,
|
|
20
|
+
value,
|
|
21
|
+
onChange,
|
|
22
|
+
}: {
|
|
23
|
+
label: string;
|
|
24
|
+
hint: string;
|
|
25
|
+
value: string;
|
|
26
|
+
onChange: (next: string) => void;
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex flex-col gap-1">
|
|
30
|
+
<Label>{label}</Label>
|
|
31
|
+
<span className="text-[11px] text-neutral-600">{hint}</span>
|
|
32
|
+
<Input value={value} onChange={(e) => onChange(e.target.value)} />
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ThisInstall({ initial }: { initial: InstallSettings }) {
|
|
38
|
+
const [form, setForm] = useState(initial);
|
|
39
|
+
const [error, setError] = useState<string | null>(null);
|
|
40
|
+
const [saved, setSaved] = useState(false);
|
|
41
|
+
const dirty = form.actor !== initial.actor || form.timezone !== initial.timezone;
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (dirty) setSaved(false);
|
|
45
|
+
}, [dirty]);
|
|
46
|
+
|
|
47
|
+
const save = () => {
|
|
48
|
+
void setInstallSettings({ ...form }).then((failure) => {
|
|
49
|
+
setError(failure);
|
|
50
|
+
setSaved(failure === null);
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<Panel title="This install">
|
|
56
|
+
<div className="flex flex-col gap-4">
|
|
57
|
+
<Field
|
|
58
|
+
label="actor"
|
|
59
|
+
hint="who caused a record — this travels with an export"
|
|
60
|
+
value={form.actor}
|
|
61
|
+
onChange={(actor) => setForm({ ...form, actor })}
|
|
62
|
+
/>
|
|
63
|
+
<Field
|
|
64
|
+
label="timezone"
|
|
65
|
+
hint="how dates are read, e.g. Europe/Budapest"
|
|
66
|
+
value={form.timezone}
|
|
67
|
+
onChange={(timezone) => setForm({ ...form, timezone })}
|
|
68
|
+
/>
|
|
69
|
+
{error && (
|
|
70
|
+
<p className="border-l-2 border-alarm bg-alarm/10 px-3 py-2 text-[12px] text-alarm">
|
|
71
|
+
{error}
|
|
72
|
+
</p>
|
|
73
|
+
)}
|
|
74
|
+
<div className="flex items-center gap-3">
|
|
75
|
+
<Button variant="primary" disabled={!dirty} onClick={save}>
|
|
76
|
+
Save
|
|
77
|
+
</Button>
|
|
78
|
+
{saved && <span className="text-[12px] text-neutral-600">saved</span>}
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</Panel>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const ATTENTION_LABELS: Record<string, string> = {
|
|
86
|
+
refusals: "A hook blocked a call, or you denied one",
|
|
87
|
+
waiting: "A handoff left a question for you",
|
|
88
|
+
unranked: "A project has no purpose on record",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const THRESHOLDS = [
|
|
92
|
+
{ key: "quietAfterDays", label: 'a project is "gone quiet" after' },
|
|
93
|
+
{ key: "urgentWithinDays", label: "a dated step is urgent within" },
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
function Ranking({ initial }: { initial: ControlRoomPrefs }) {
|
|
97
|
+
const [prefs, setPrefs] = useState(initial);
|
|
98
|
+
const [error, setError] = useState<string | null>(null);
|
|
99
|
+
|
|
100
|
+
const save = (update: Record<string, unknown>) => {
|
|
101
|
+
void writePrefs(update).then((failure) => {
|
|
102
|
+
setError(failure);
|
|
103
|
+
if (!failure) setPrefs({ ...prefs, ...update } as ControlRoomPrefs);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<>
|
|
109
|
+
<Panel title="Ranking">
|
|
110
|
+
<div className="flex flex-col gap-4">
|
|
111
|
+
{THRESHOLDS.map(({ key, label }) => (
|
|
112
|
+
<div key={key} className="flex flex-col gap-1">
|
|
113
|
+
<Label>{label}</Label>
|
|
114
|
+
<div className="flex items-center gap-2">
|
|
115
|
+
<Input
|
|
116
|
+
type="number"
|
|
117
|
+
min={1}
|
|
118
|
+
max={365}
|
|
119
|
+
className="w-20"
|
|
120
|
+
value={prefs[key]}
|
|
121
|
+
onChange={(e) => {
|
|
122
|
+
const days = Number(e.target.value);
|
|
123
|
+
setPrefs({ ...prefs, [key]: days });
|
|
124
|
+
if (Number.isInteger(days) && days > 0) save({ [key]: days });
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
<span className="text-[12px] text-neutral-700">days</span>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
))}
|
|
131
|
+
<div className="flex items-center justify-between gap-3 border-t border-divider pt-3 text-[12.5px]">
|
|
132
|
+
<span>rank stated goals in the grid, not just projects</span>
|
|
133
|
+
<Switch
|
|
134
|
+
aria-label="rank stated goals in the grid"
|
|
135
|
+
checked={prefs.rankGoals}
|
|
136
|
+
onCheckedChange={(rankGoals) => save({ rankGoals })}
|
|
137
|
+
/>
|
|
138
|
+
</div>
|
|
139
|
+
{error && (
|
|
140
|
+
<p className="border-l-2 border-alarm bg-alarm/10 px-3 py-2 text-[12px] text-alarm">
|
|
141
|
+
{error}
|
|
142
|
+
</p>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</Panel>
|
|
146
|
+
<Panel title="Attention">
|
|
147
|
+
<p className="mb-3 text-[12px] text-neutral-700">
|
|
148
|
+
What lights the bell. Each source is a fact PAL already holds — nothing here
|
|
149
|
+
asks a model.
|
|
150
|
+
</p>
|
|
151
|
+
<div className="flex flex-col">
|
|
152
|
+
{Object.entries(prefs.attention).map(([source, on]) => (
|
|
153
|
+
<div
|
|
154
|
+
key={source}
|
|
155
|
+
className="flex items-center justify-between gap-3 border-t border-divider py-2.5 text-[12.5px] first:border-t-0"
|
|
156
|
+
>
|
|
157
|
+
<span>{ATTENTION_LABELS[source] ?? source}</span>
|
|
158
|
+
<Switch
|
|
159
|
+
aria-label={ATTENTION_LABELS[source] ?? source}
|
|
160
|
+
checked={on}
|
|
161
|
+
onCheckedChange={(next) => save({ attention: { [source]: next } })}
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
</Panel>
|
|
167
|
+
</>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function Settings() {
|
|
172
|
+
const settings = useLoaded<InstallSettings>("/api/settings");
|
|
173
|
+
const prefs = useLoaded<ControlRoomPrefs>("/api/prefs");
|
|
174
|
+
const status = useLoaded<ServerStatus>("/api/status");
|
|
175
|
+
|
|
176
|
+
if (settings.state !== "ready") return <Pending value={settings} />;
|
|
177
|
+
return (
|
|
178
|
+
<div className="grid items-start gap-6 md:grid-cols-2">
|
|
179
|
+
<ThisInstall initial={settings.data} />
|
|
180
|
+
{prefs.state === "ready" && <Ranking initial={prefs.data} />}
|
|
181
|
+
<Panel title="Where it runs">
|
|
182
|
+
{status.state === "ready" ? (
|
|
183
|
+
<dl className="m-0 grid grid-cols-[110px_1fr] gap-y-1.5 text-[12.5px]">
|
|
184
|
+
<dt className="text-neutral-600">machine</dt>
|
|
185
|
+
<dd className="m-0">{status.data.machine}</dd>
|
|
186
|
+
<dt className="text-neutral-600">port</dt>
|
|
187
|
+
<dd className="m-0 tabular-nums">{status.data.port}</dd>
|
|
188
|
+
<dt className="text-neutral-600">pid</dt>
|
|
189
|
+
<dd className="m-0 tabular-nums">{status.data.pid}</dd>
|
|
190
|
+
<dt className="text-neutral-600">ledger</dt>
|
|
191
|
+
<dd className="m-0">{status.data.ledgerFiles} file(s)</dd>
|
|
192
|
+
<dt className="text-neutral-600">up since</dt>
|
|
193
|
+
<dd className="m-0">{status.data.startedAt}</dd>
|
|
194
|
+
</dl>
|
|
195
|
+
) : (
|
|
196
|
+
<Empty>not answering</Empty>
|
|
197
|
+
)}
|
|
198
|
+
<p className="mt-3 text-[11px] text-neutral-600">
|
|
199
|
+
Loopback only. The machine label is set with <code>pal cli machine</code>, and
|
|
200
|
+
stays on this machine.
|
|
201
|
+
</p>
|
|
202
|
+
</Panel>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { AgendaView, HandoffCard, SignalView, SummaryView } from "../../data";
|
|
3
|
+
import type { Matrix, MatrixItem } from "../../matrix";
|
|
4
|
+
import { Badge } from "../components/badge";
|
|
5
|
+
import {
|
|
6
|
+
Table,
|
|
7
|
+
TableBody,
|
|
8
|
+
TableCell,
|
|
9
|
+
TableHead,
|
|
10
|
+
TableHeader,
|
|
11
|
+
TableRow,
|
|
12
|
+
} from "../components/table";
|
|
13
|
+
import { tenths } from "../format";
|
|
14
|
+
import { Empty, Panel, Pending } from "../frame";
|
|
15
|
+
import { type Loaded, useLoaded } from "../lib/api";
|
|
16
|
+
import { cn } from "../lib/cn";
|
|
17
|
+
import { GoalsList, HandoffList, ItemCard, MovesList, ReasonTags } from "../parts";
|
|
18
|
+
|
|
19
|
+
export const LAYOUTS = [
|
|
20
|
+
{ value: "matrix", label: "matrix" },
|
|
21
|
+
{ value: "list", label: "list" },
|
|
22
|
+
{ value: "briefing", label: "briefing" },
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export type Layout = (typeof LAYOUTS)[number]["value"];
|
|
26
|
+
|
|
27
|
+
export function isLayout(value: string | null): value is Layout {
|
|
28
|
+
return LAYOUTS.some((l) => l.value === value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const QUADRANTS = [
|
|
32
|
+
{ id: "now", title: "Do now", note: "matters and cannot wait", lit: true },
|
|
33
|
+
{
|
|
34
|
+
id: "plan",
|
|
35
|
+
title: "Give it a slot",
|
|
36
|
+
note: "matters, nothing forcing it",
|
|
37
|
+
lit: false,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "noise",
|
|
41
|
+
title: "Loud, not load-bearing",
|
|
42
|
+
note: "pressing but serves nothing",
|
|
43
|
+
lit: false,
|
|
44
|
+
},
|
|
45
|
+
{ id: "later", title: "Let it sit", note: "neither", lit: false },
|
|
46
|
+
] as const;
|
|
47
|
+
|
|
48
|
+
const FORTNIGHT_DAYS = 14;
|
|
49
|
+
|
|
50
|
+
function goalsOf(matrix: Matrix): MatrixItem[] {
|
|
51
|
+
return [...matrix.now, ...matrix.plan, ...matrix.noise, ...matrix.later].filter(
|
|
52
|
+
(i) => i.kind === "goal"
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function rankedOf(matrix: Matrix): { item: MatrixItem; quadrant: string }[] {
|
|
57
|
+
return QUADRANTS.flatMap(({ id, title }) =>
|
|
58
|
+
(matrix[id] as MatrixItem[]).map((item) => ({ item, quadrant: title }))
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function Quadrant({
|
|
63
|
+
title,
|
|
64
|
+
note,
|
|
65
|
+
lit,
|
|
66
|
+
items,
|
|
67
|
+
onSaved,
|
|
68
|
+
compact,
|
|
69
|
+
}: {
|
|
70
|
+
title: string;
|
|
71
|
+
note: string;
|
|
72
|
+
lit: boolean;
|
|
73
|
+
items: MatrixItem[];
|
|
74
|
+
onSaved: () => void;
|
|
75
|
+
compact?: boolean;
|
|
76
|
+
}) {
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className={cn(
|
|
80
|
+
"blueprint flex flex-col gap-2 p-4",
|
|
81
|
+
lit ? "bg-accent-100" : "bg-transparent",
|
|
82
|
+
compact ? "min-h-[120px]" : "min-h-[160px]"
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
<div className="flex items-baseline justify-between gap-2">
|
|
86
|
+
<h3 className="m-0 text-[20px]">{title}</h3>
|
|
87
|
+
<span className="text-[11px] text-neutral-700">{note}</span>
|
|
88
|
+
</div>
|
|
89
|
+
{items.length === 0 ? (
|
|
90
|
+
<Empty>nothing here</Empty>
|
|
91
|
+
) : (
|
|
92
|
+
items.map((item) => (
|
|
93
|
+
<ItemCard key={`${item.kind}:${item.id}`} item={item} onSaved={onSaved} />
|
|
94
|
+
))
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function Fortnight() {
|
|
101
|
+
const view = useLoaded<SummaryView>(`/api/summary?days=${FORTNIGHT_DAYS}`);
|
|
102
|
+
const s = view.state === "ready" ? view.data : null;
|
|
103
|
+
return (
|
|
104
|
+
<Panel title={`Last ${FORTNIGHT_DAYS} days`}>
|
|
105
|
+
<dl className="m-0 grid grid-cols-3 gap-2">
|
|
106
|
+
<Figure n={s ? String(s.actions) : "—"} label="actions" />
|
|
107
|
+
<Figure n={s ? String(s.refusals) : "—"} label="refusals" />
|
|
108
|
+
<Figure
|
|
109
|
+
n={s?.rating === null || !s ? "—" : tenths(s.rating)}
|
|
110
|
+
label="rating, last 10"
|
|
111
|
+
/>
|
|
112
|
+
</dl>
|
|
113
|
+
</Panel>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function Figure({ n, label }: { n: string; label: string }) {
|
|
118
|
+
return (
|
|
119
|
+
<div>
|
|
120
|
+
<dt className="font-heading text-[26px] leading-none font-semibold tabular-nums">
|
|
121
|
+
{n}
|
|
122
|
+
</dt>
|
|
123
|
+
<dd className="m-0 text-[11px] text-neutral-600">{label}</dd>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function DueBadges() {
|
|
129
|
+
const signal = useLoaded<SignalView>("/api/signal");
|
|
130
|
+
if (signal.state !== "ready") return <Pending value={signal} />;
|
|
131
|
+
const due = Object.entries(signal.data.due).filter(([, b]) => b.state === "due");
|
|
132
|
+
if (due.length === 0) return <Empty>Nothing is due.</Empty>;
|
|
133
|
+
return (
|
|
134
|
+
<div className="flex flex-col gap-2">
|
|
135
|
+
{due.map(([name, badge]) => (
|
|
136
|
+
<div key={name} className="text-[12px]">
|
|
137
|
+
<Badge variant="accent">{name}</Badge>
|
|
138
|
+
<span className="ml-2 text-neutral-700">{badge.detail}</span>
|
|
139
|
+
</div>
|
|
140
|
+
))}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function Sidebar({ matrix, handoffs }: { matrix: Matrix; handoffs: HandoffCard[] }) {
|
|
146
|
+
return (
|
|
147
|
+
<aside className="flex flex-col gap-6">
|
|
148
|
+
<Panel title="Where you left off">
|
|
149
|
+
<HandoffList cards={handoffs} />
|
|
150
|
+
</Panel>
|
|
151
|
+
<Panel title="Goals">
|
|
152
|
+
<GoalsList goals={goalsOf(matrix)} />
|
|
153
|
+
</Panel>
|
|
154
|
+
<Fortnight />
|
|
155
|
+
<Panel title="Needs a run">
|
|
156
|
+
<DueBadges />
|
|
157
|
+
</Panel>
|
|
158
|
+
</aside>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function MatrixLayout({
|
|
163
|
+
matrix,
|
|
164
|
+
handoffs,
|
|
165
|
+
moves,
|
|
166
|
+
onSaved,
|
|
167
|
+
}: {
|
|
168
|
+
matrix: Matrix;
|
|
169
|
+
handoffs: HandoffCard[];
|
|
170
|
+
moves: AgendaView["moves"];
|
|
171
|
+
onSaved: () => void;
|
|
172
|
+
}) {
|
|
173
|
+
return (
|
|
174
|
+
<div className="grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_300px]">
|
|
175
|
+
<div>
|
|
176
|
+
<div className="mb-6 grid gap-px border border-divider bg-divider md:grid-cols-3">
|
|
177
|
+
{moves.map((move, i) => (
|
|
178
|
+
<div
|
|
179
|
+
key={move.move}
|
|
180
|
+
className="grid grid-cols-[26px_1fr] gap-2 bg-bg px-4 py-3"
|
|
181
|
+
>
|
|
182
|
+
<span className="font-heading text-[22px] leading-none font-semibold text-accent">
|
|
183
|
+
{String(i + 1).padStart(2, "0")}
|
|
184
|
+
</span>
|
|
185
|
+
<span>
|
|
186
|
+
<span className="block font-medium text-pretty">{move.move}</span>
|
|
187
|
+
<span className="text-[12px] text-neutral-700">{move.because}</span>
|
|
188
|
+
</span>
|
|
189
|
+
</div>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
<div className="grid grid-cols-[18px_minmax(0,1fr)_minmax(0,1fr)]">
|
|
193
|
+
<div />
|
|
194
|
+
<div className="eyebrow pb-1.5 pl-3">urgent</div>
|
|
195
|
+
<div className="eyebrow pb-1.5 pl-3">not urgent</div>
|
|
196
|
+
<div className="eyebrow [writing-mode:vertical-rl] rotate-180 text-center">
|
|
197
|
+
important
|
|
198
|
+
</div>
|
|
199
|
+
{QUADRANTS.slice(0, 2).map((q) => (
|
|
200
|
+
<Quadrant
|
|
201
|
+
key={q.id}
|
|
202
|
+
{...q}
|
|
203
|
+
items={matrix[q.id] as MatrixItem[]}
|
|
204
|
+
onSaved={onSaved}
|
|
205
|
+
/>
|
|
206
|
+
))}
|
|
207
|
+
<div className="eyebrow [writing-mode:vertical-rl] rotate-180 text-center">
|
|
208
|
+
not important
|
|
209
|
+
</div>
|
|
210
|
+
{QUADRANTS.slice(2).map((q) => (
|
|
211
|
+
<Quadrant
|
|
212
|
+
key={q.id}
|
|
213
|
+
{...q}
|
|
214
|
+
items={matrix[q.id] as MatrixItem[]}
|
|
215
|
+
onSaved={onSaved}
|
|
216
|
+
/>
|
|
217
|
+
))}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
<Sidebar matrix={matrix} handoffs={handoffs} />
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function ListLayout({
|
|
226
|
+
matrix,
|
|
227
|
+
handoffs,
|
|
228
|
+
moves,
|
|
229
|
+
}: {
|
|
230
|
+
matrix: Matrix;
|
|
231
|
+
handoffs: HandoffCard[];
|
|
232
|
+
moves: AgendaView["moves"];
|
|
233
|
+
onSaved: () => void;
|
|
234
|
+
}) {
|
|
235
|
+
return (
|
|
236
|
+
<div className="grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_260px]">
|
|
237
|
+
<div className="blueprint overflow-x-auto bg-bg">
|
|
238
|
+
<Table className="min-w-[720px] text-[13px]">
|
|
239
|
+
<TableHeader>
|
|
240
|
+
<tr>
|
|
241
|
+
{["Item", "Placed", "Why", "Next"].map((column) => (
|
|
242
|
+
<TableHead key={column}>{column}</TableHead>
|
|
243
|
+
))}
|
|
244
|
+
</tr>
|
|
245
|
+
</TableHeader>
|
|
246
|
+
<TableBody>
|
|
247
|
+
{rankedOf(matrix).map(({ item, quadrant }) => (
|
|
248
|
+
<TableRow key={`${item.kind}:${item.id}`}>
|
|
249
|
+
<TableCell>
|
|
250
|
+
<span className="font-heading text-[15px] font-semibold">
|
|
251
|
+
{item.label}
|
|
252
|
+
</span>
|
|
253
|
+
</TableCell>
|
|
254
|
+
<TableCell className="whitespace-nowrap">{quadrant}</TableCell>
|
|
255
|
+
<TableCell className="min-w-[200px]">
|
|
256
|
+
<ReasonTags item={item} />
|
|
257
|
+
</TableCell>
|
|
258
|
+
<TableCell className="max-w-[320px] text-[12px] text-neutral-800">
|
|
259
|
+
{item.detail}
|
|
260
|
+
</TableCell>
|
|
261
|
+
</TableRow>
|
|
262
|
+
))}
|
|
263
|
+
</TableBody>
|
|
264
|
+
</Table>
|
|
265
|
+
</div>
|
|
266
|
+
<aside className="flex flex-col gap-6">
|
|
267
|
+
<Panel title="Three moves">
|
|
268
|
+
<MovesList moves={moves} />
|
|
269
|
+
</Panel>
|
|
270
|
+
<Panel title="Grid">
|
|
271
|
+
<div className="grid grid-cols-2 gap-1">
|
|
272
|
+
{QUADRANTS.map((q) => (
|
|
273
|
+
<div key={q.id} className="border border-divider p-2">
|
|
274
|
+
<span className="font-heading block text-[24px] leading-none font-semibold">
|
|
275
|
+
{(matrix[q.id] as MatrixItem[]).length}
|
|
276
|
+
</span>
|
|
277
|
+
<span className="eyebrow">{q.title}</span>
|
|
278
|
+
</div>
|
|
279
|
+
))}
|
|
280
|
+
</div>
|
|
281
|
+
</Panel>
|
|
282
|
+
<Panel title="Where you left off">
|
|
283
|
+
<HandoffList cards={handoffs} />
|
|
284
|
+
</Panel>
|
|
285
|
+
</aside>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function BriefingLayout({
|
|
291
|
+
matrix,
|
|
292
|
+
handoffs,
|
|
293
|
+
moves,
|
|
294
|
+
onSaved,
|
|
295
|
+
}: {
|
|
296
|
+
matrix: Matrix;
|
|
297
|
+
handoffs: HandoffCard[];
|
|
298
|
+
moves: AgendaView["moves"];
|
|
299
|
+
onSaved: () => void;
|
|
300
|
+
}) {
|
|
301
|
+
return (
|
|
302
|
+
<div className="grid items-start gap-8 lg:grid-cols-[minmax(0,7fr)_minmax(0,5fr)]">
|
|
303
|
+
<div className="flex flex-col gap-8">
|
|
304
|
+
<div>
|
|
305
|
+
<h6 className="eyebrow mb-2">If you only do three things</h6>
|
|
306
|
+
<MovesList moves={moves} size="large" />
|
|
307
|
+
</div>
|
|
308
|
+
<div>
|
|
309
|
+
<h6 className="eyebrow mb-2">Where you left off</h6>
|
|
310
|
+
<HandoffList cards={handoffs} />
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
<div className="flex flex-col gap-6">
|
|
314
|
+
<div>
|
|
315
|
+
<h6 className="eyebrow mb-2">The grid</h6>
|
|
316
|
+
<div className="grid grid-cols-2 gap-1.5">
|
|
317
|
+
{QUADRANTS.map((q) => (
|
|
318
|
+
<Quadrant
|
|
319
|
+
key={q.id}
|
|
320
|
+
{...q}
|
|
321
|
+
compact
|
|
322
|
+
items={matrix[q.id] as MatrixItem[]}
|
|
323
|
+
onSaved={onSaved}
|
|
324
|
+
/>
|
|
325
|
+
))}
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
<Panel title="Goals">
|
|
329
|
+
<GoalsList goals={goalsOf(matrix)} />
|
|
330
|
+
</Panel>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function Heading({ agenda, matrix }: { agenda: AgendaView; matrix: Matrix }) {
|
|
337
|
+
const written = agenda.generatedAt
|
|
338
|
+
? `agenda written ${agenda.ageHours ?? 0}h ago at session stop`
|
|
339
|
+
: "no agenda written yet";
|
|
340
|
+
return (
|
|
341
|
+
<div className="mb-5 flex flex-wrap items-baseline justify-between gap-4">
|
|
342
|
+
<div className="flex flex-wrap items-baseline gap-4">
|
|
343
|
+
<h1 className="m-0 text-[34px]">
|
|
344
|
+
{new Date().toLocaleDateString(undefined, {
|
|
345
|
+
weekday: "long",
|
|
346
|
+
day: "numeric",
|
|
347
|
+
month: "long",
|
|
348
|
+
})}
|
|
349
|
+
</h1>
|
|
350
|
+
<span
|
|
351
|
+
className={cn("text-[12px]", agenda.stale ? "text-alarm" : "text-neutral-700")}
|
|
352
|
+
>
|
|
353
|
+
{written} · {matrix.now.length} items need you
|
|
354
|
+
</span>
|
|
355
|
+
</div>
|
|
356
|
+
{matrix.unranked > 0 && (
|
|
357
|
+
<span className="text-[12px] text-neutral-700">
|
|
358
|
+
{matrix.unranked} project(s) with no purpose on record
|
|
359
|
+
</span>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function Today({ layout }: { layout: Layout }) {
|
|
366
|
+
const [nonce, setNonce] = useState(0);
|
|
367
|
+
const matrix = useLoaded<Matrix>(`/api/matrix?v=${nonce}`);
|
|
368
|
+
const agenda = useLoaded<AgendaView>("/api/agenda");
|
|
369
|
+
const handoffs = useLoaded<HandoffCard[]>("/api/handoffs");
|
|
370
|
+
const reload = () => setNonce((n) => n + 1);
|
|
371
|
+
|
|
372
|
+
const waiting: Loaded<unknown>[] = [matrix, agenda, handoffs];
|
|
373
|
+
const notReady = waiting.find((v) => v.state !== "ready");
|
|
374
|
+
if (notReady || matrix.state !== "ready") return <Pending value={notReady ?? matrix} />;
|
|
375
|
+
if (agenda.state !== "ready" || handoffs.state !== "ready") return null;
|
|
376
|
+
|
|
377
|
+
const shared = {
|
|
378
|
+
matrix: matrix.data,
|
|
379
|
+
handoffs: handoffs.data,
|
|
380
|
+
moves: agenda.data.moves,
|
|
381
|
+
onSaved: reload,
|
|
382
|
+
};
|
|
383
|
+
return (
|
|
384
|
+
<section>
|
|
385
|
+
<Heading agenda={agenda.data} matrix={matrix.data} />
|
|
386
|
+
{layout === "matrix" && <MatrixLayout {...shared} />}
|
|
387
|
+
{layout === "list" && <ListLayout {...shared} />}
|
|
388
|
+
{layout === "briefing" && <BriefingLayout {...shared} />}
|
|
389
|
+
</section>
|
|
390
|
+
);
|
|
391
|
+
}
|