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.
Files changed (69) hide show
  1. package/package.json +10 -7
  2. package/src/cli/index.ts +21 -2
  3. package/src/cli/server.ts +33 -0
  4. package/src/hooks/handlers/agenda.ts +195 -7
  5. package/src/hooks/handlers/update-check.ts +1 -1
  6. package/src/hooks/lib/agenda-store.ts +2 -0
  7. package/src/hooks/lib/goal-links.ts +83 -0
  8. package/src/hooks/lib/models.ts +1 -1
  9. package/src/hooks/lib/projects.ts +5 -0
  10. package/src/hooks/lib/settings.ts +2 -0
  11. package/src/hooks/lib/telos-goals.ts +8 -2
  12. package/src/hooks/lib/token-usage.ts +2 -0
  13. package/src/tools/agent/project.ts +22 -20
  14. package/src/tools/control-room/attention.ts +146 -0
  15. package/src/tools/control-room/data.ts +76 -5
  16. package/src/tools/control-room/detail.ts +158 -0
  17. package/src/tools/control-room/matrix.ts +91 -19
  18. package/src/tools/control-room/prefs.ts +96 -0
  19. package/src/tools/control-room/server-config.ts +8 -0
  20. package/src/tools/control-room/server.ts +196 -11
  21. package/src/tools/control-room/snooze.ts +65 -0
  22. package/src/tools/control-room/static.ts +68 -0
  23. package/src/tools/control-room/ui/app.tsx +196 -50
  24. package/src/tools/control-room/ui/attention-bell.tsx +118 -0
  25. package/src/tools/control-room/ui/components/README.md +18 -0
  26. package/src/tools/control-room/ui/components/badge.tsx +38 -0
  27. package/src/tools/control-room/ui/components/button.tsx +43 -0
  28. package/src/tools/control-room/ui/components/card.tsx +45 -0
  29. package/src/tools/control-room/ui/components/input.tsx +24 -0
  30. package/src/tools/control-room/ui/components/label.tsx +18 -0
  31. package/src/tools/control-room/ui/components/popover.tsx +37 -0
  32. package/src/tools/control-room/ui/components/separator.tsx +25 -0
  33. package/src/tools/control-room/ui/components/skeleton.tsx +14 -0
  34. package/src/tools/control-room/ui/components/switch.tsx +27 -0
  35. package/src/tools/control-room/ui/components/table.tsx +60 -0
  36. package/src/tools/control-room/ui/components/toggle-group.tsx +38 -0
  37. package/src/tools/control-room/ui/dist/assets/index-BoQjFpzV.js +49 -0
  38. package/src/tools/control-room/ui/dist/assets/index-Dncp2bYg.css +2 -0
  39. package/src/tools/control-room/ui/dist/index.html +19 -0
  40. package/src/tools/control-room/ui/format.ts +0 -12
  41. package/src/tools/control-room/ui/frame.tsx +125 -0
  42. package/src/tools/control-room/ui/index.html +2 -2
  43. package/src/tools/control-room/ui/lib/api.ts +27 -0
  44. package/src/tools/control-room/ui/lib/cn.ts +6 -0
  45. package/src/tools/control-room/ui/lib/write.ts +43 -0
  46. package/src/tools/control-room/ui/package.json +28 -0
  47. package/src/tools/control-room/ui/parts.tsx +235 -0
  48. package/src/tools/control-room/ui/screens/log.tsx +274 -0
  49. package/src/tools/control-room/ui/screens/project-detail.tsx +302 -0
  50. package/src/tools/control-room/ui/screens/projects.tsx +128 -0
  51. package/src/tools/control-room/ui/screens/settings.tsx +205 -0
  52. package/src/tools/control-room/ui/screens/today.tsx +391 -0
  53. package/src/tools/control-room/ui/theme.css +127 -0
  54. package/src/tools/control-room/ui/vite.config.ts +36 -0
  55. package/src/tools/control-room/writes.ts +106 -0
  56. package/src/tools/ledger/outcomes.ts +9 -0
  57. package/src/tools/ledger/query.ts +2 -0
  58. package/src/tools/ledger/view.ts +31 -10
  59. package/src/tools/lib/handoff-note.ts +14 -0
  60. package/src/tools/lib/project-isc.ts +61 -0
  61. package/src/tools/control-room/ui/agenda.tsx +0 -43
  62. package/src/tools/control-room/ui/agents.tsx +0 -67
  63. package/src/tools/control-room/ui/app.css +0 -857
  64. package/src/tools/control-room/ui/board.tsx +0 -82
  65. package/src/tools/control-room/ui/handoffs.tsx +0 -37
  66. package/src/tools/control-room/ui/ledger.tsx +0 -137
  67. package/src/tools/control-room/ui/matrix.tsx +0 -117
  68. package/src/tools/control-room/ui/panel.tsx +0 -60
  69. package/src/tools/control-room/ui/signal.tsx +0 -161
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, Menlo, monospace;--color-neutral-100:#f5f5f8;--color-neutral-200:#e7e7ea;--color-neutral-300:#d4d4d7;--color-neutral-400:#b7b7ba;--color-neutral-500:#98989b;--color-neutral-600:#7a7a7d;--color-neutral-700:#5d5d60;--color-neutral-800:#424244;--color-neutral-900:#2b2b2d;--spacing:.2125rem;--font-weight-medium:500;--font-weight-semibold:600;--tracking-wide:.025em;--leading-tight:1.25;--radius-sm:2px;--radius-md:4px;--radius-lg:7px;--shadow-sm:0 1px 2px #2b2b2d24;--shadow-md:0 3px 10px #2b2b2d29;--shadow-lg:0 12px 32px #2b2b2d38;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-bg:#f2f2f3;--color-surface:#e9e9ea;--color-ink:#1d1f20;--color-divider:#1d1f2029;--color-accent:#5980a6;--color-accent-100:#eef6ff;--color-accent-200:#d6ebff;--color-accent-300:#b5d9fd;--color-accent-400:#94bce3;--color-accent-500:#749dc4;--color-accent-600:#597ea3;--color-accent-700:#416180;--color-accent-800:#2c455d;--color-accent-900:#1d2d3d;--color-alarm:oklch(55% .17 27);--font-heading:"Barlow Condensed", system-ui, sans-serif;--font-body:"Barlow", system-ui, sans-serif;--registration-size:11px;--registration-inset:-6px;--registration-mark:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='11'%3E%3Cpath d='M5.5 0v11M0 5.5h11' stroke='%231d1f20' stroke-opacity='.55' stroke-width='1'/%3E%3C/svg%3E")}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}body{background:var(--color-bg);color:var(--color-ink);font-family:var(--font-body);font-size:13px;line-height:1.45}h1,h2,h3,h4,h5,h6{font-family:var(--font-heading);letter-spacing:.02em;font-weight:600}:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}::selection{background:var(--color-accent-200)}}@layer components;@layer utilities{.blueprint{border:1px solid var(--color-divider);border-radius:0;position:relative}.blueprint:after{content:"";inset:var(--registration-inset);pointer-events:none;background-image:var(--registration-mark), var(--registration-mark), var(--registration-mark), var(--registration-mark);background-repeat:no-repeat;background-size:var(--registration-size) var(--registration-size);background-position:0 0,100% 0,0 100%,100% 100%;position:absolute}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.-top-1{top:calc(var(--spacing) * -1)}.top-0{top:0}.-right-1{right:calc(var(--spacing) * -1)}.z-10{z-index:10}.z-50{z-index:50}.-m-2{margin:calc(var(--spacing) * -2)}.m-0{margin:0}.mx-auto{margin-inline:auto}.my-3{margin-block:calc(var(--spacing) * 3)}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.-mb-\[11px\]{margin-bottom:-11px}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-5{margin-bottom:calc(var(--spacing) * 5)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.block{display:block}.flex{display:flex}.grid{display:grid}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.table-cell{display:table-cell}.table-row{display:table-row}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2\.5{width:calc(var(--spacing) * 2.5);height:calc(var(--spacing) * 2.5)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-8{width:calc(var(--spacing) * 8);height:calc(var(--spacing) * 8)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-3{height:calc(var(--spacing) * 3)}.h-4{height:calc(var(--spacing) * 4)}.h-6{height:calc(var(--spacing) * 6)}.h-8{height:calc(var(--spacing) * 8)}.h-auto{height:auto}.h-screen{height:100vh}.max-h-\[70vh\]{max-height:70vh}.min-h-0{min-height:0}.min-h-\[120px\]{min-height:120px}.min-h-\[160px\]{min-height:160px}.w-1\/3{width:33.3333%}.w-2\/5{width:40%}.w-3\/5{width:60%}.w-7{width:calc(var(--spacing) * 7)}.w-20{width:calc(var(--spacing) * 20)}.w-\[380px\]{width:380px}.w-fit{width:fit-content}.w-full{width:100%}.max-w-\[220px\]{max-width:220px}.max-w-\[320px\]{max-width:320px}.max-w-\[760px\]{max-width:760px}.max-w-\[1480px\]{max-width:1480px}.min-w-4{min-width:calc(var(--spacing) * 4)}.min-w-\[120px\]{min-width:120px}.min-w-\[200px\]{min-width:200px}.min-w-\[720px\]{min-width:720px}.min-w-\[880px\]{min-width:880px}.flex-1{flex:1}.shrink-0{flex-shrink:0}.border-separate{border-collapse:separate}.border-spacing-0{--tw-border-spacing-x:0px;--tw-border-spacing-y:0px;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.rotate-180{rotate:180deg}.animate-pulse{animation:var(--animate-pulse)}.cursor-pointer{cursor:pointer}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.list-none{list-style-type:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-\[8px_1fr_auto\]{grid-template-columns:8px 1fr auto}.grid-cols-\[18px_58px_minmax\(0\,1fr\)\]{grid-template-columns:18px 58px minmax(0,1fr)}.grid-cols-\[18px_minmax\(0\,1fr\)_minmax\(0\,1fr\)\]{grid-template-columns:18px minmax(0,1fr) minmax(0,1fr)}.grid-cols-\[26px_1fr\]{grid-template-columns:26px 1fr}.grid-cols-\[44px_1fr\]{grid-template-columns:44px 1fr}.grid-cols-\[70px_1fr_32px\]{grid-template-columns:70px 1fr 32px}.grid-cols-\[78px_1fr\]{grid-template-columns:78px 1fr}.grid-cols-\[86px_1fr_auto\]{grid-template-columns:86px 1fr auto}.grid-cols-\[110px_1fr\]{grid-template-columns:110px 1fr}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0{gap:0}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-px{gap:1px}.gap-y-1\.5{row-gap:calc(var(--spacing) * 1.5)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded-none{border-radius:0}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-accent{border-color:var(--color-accent)}.border-alarm{border-color:var(--color-alarm)}.border-alarm\/40{border-color:#c13c3666}@supports (color:color-mix(in lab, red, red)){.border-alarm\/40{border-color:color-mix(in oklab, var(--color-alarm) 40%, transparent)}}.border-divider{border-color:var(--color-divider)}.border-divider\/60{border-color:#1d1f2019}@supports (color:color-mix(in lab, red, red)){.border-divider\/60{border-color:color-mix(in oklab, var(--color-divider) 60%, transparent)}}.border-neutral-500{border-color:var(--color-neutral-500)}.border-transparent{border-color:#0000}.bg-accent{background-color:var(--color-accent)}.bg-accent-100{background-color:var(--color-accent-100)}.bg-accent-900{background-color:var(--color-accent-900)}.bg-alarm{background-color:var(--color-alarm)}.bg-alarm\/10{background-color:#c13c361a}@supports (color:color-mix(in lab, red, red)){.bg-alarm\/10{background-color:color-mix(in oklab, var(--color-alarm) 10%, transparent)}}.bg-bg{background-color:var(--color-bg)}.bg-divider{background-color:var(--color-divider)}.bg-neutral-200{background-color:var(--color-neutral-200)}.bg-neutral-300{background-color:var(--color-neutral-300)}.bg-neutral-400{background-color:var(--color-neutral-400)}.bg-transparent{background-color:#0000}.p-0{padding:0}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-4{padding:calc(var(--spacing) * 4)}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-7{padding-inline:calc(var(--spacing) * 7)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-px{padding-top:1px}.pb-1\.5{padding-bottom:calc(var(--spacing) * 1.5)}.pb-6{padding-bottom:calc(var(--spacing) * 6)}.pl-3{padding-left:calc(var(--spacing) * 3)}.pl-4{padding-left:calc(var(--spacing) * 4)}.pl-5{padding-left:calc(var(--spacing) * 5)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-top{vertical-align:top}.font-heading{font-family:var(--font-heading)}.font-mono{font-family:var(--font-mono)}.eyebrow{letter-spacing:.12em;text-transform:uppercase;color:var(--color-neutral-700);font-size:10px}.text-\[10\.5px\]{font-size:10.5px}.text-\[10px\]{font-size:10px}.text-\[11\.5px\]{font-size:11.5px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.text-\[16px\]{font-size:16px}.text-\[20px\]{font-size:20px}.text-\[22px\]{font-size:22px}.text-\[24px\]{font-size:24px}.text-\[26px\]{font-size:26px}.text-\[34px\]{font-size:34px}.text-\[38px\]{font-size:38px}.text-\[40px\]{font-size:40px}.leading-none{--tw-leading:1;line-height:1}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.text-pretty{text-wrap:pretty}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-accent{color:var(--color-accent)}.text-accent-900{color:var(--color-accent-900)}.text-alarm{color:var(--color-alarm)}.text-bg{color:var(--color-bg)}.text-ink{color:var(--color-ink)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-600{color:var(--color-neutral-600)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-800{color:var(--color-neutral-800)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.opacity-55{opacity:.55}.opacity-60{opacity:.6}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-\[0_1px_0_var\(--color-divider\)\]{--tw-shadow:0 1px 0 var(--tw-shadow-color,var(--color-divider));box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-lg{--tw-shadow:0 12px 32px var(--tw-shadow-color,#2b2b2d38);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.\[writing-mode\:vertical-rl\]{writing-mode:vertical-rl}.first\:border-t-0:first-child{border-top-style:var(--tw-border-style);border-top-width:0}.first\:border-l-0:first-child{border-left-style:var(--tw-border-style);border-left-width:0}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}@media (hover:hover){.hover\:border-accent:hover{border-color:var(--color-accent)}.hover\:border-divider:hover{border-color:var(--color-divider)}.hover\:bg-accent-600:hover{background-color:var(--color-accent-600)}.hover\:bg-neutral-200:hover{background-color:var(--color-neutral-200)}.hover\:bg-neutral-200\/50:hover{background-color:#e7e7ea80}@supports (color:color-mix(in lab, red, red)){.hover\:bg-neutral-200\/50:hover{background-color:color-mix(in oklab, var(--color-neutral-200) 50%, transparent)}}.hover\:text-accent:hover{color:var(--color-accent)}}.focus-visible\:border-accent:focus-visible{border-color:var(--color-accent)}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-default:disabled{cursor:default}.disabled\:opacity-45:disabled{opacity:.45}.data-\[orientation\=horizontal\]\:h-px[data-orientation=horizontal]{height:1px}.data-\[orientation\=horizontal\]\:w-full[data-orientation=horizontal]{width:100%}.data-\[orientation\=vertical\]\:h-full[data-orientation=vertical]{height:100%}.data-\[orientation\=vertical\]\:w-px[data-orientation=vertical]{width:1px}.data-\[state\=checked\]\:translate-x-3\.5[data-state=checked]{--tw-translate-x:calc(var(--spacing) * 3.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[state\=checked\]\:bg-accent[data-state=checked]{background-color:var(--color-accent)}.data-\[state\=checked\]\:bg-bg[data-state=checked]{background-color:var(--color-bg)}.data-\[state\=on\]\:bg-accent[data-state=on]{background-color:var(--color-accent)}.data-\[state\=on\]\:text-bg[data-state=on]{color:var(--color-bg)}.data-\[state\=unchecked\]\:translate-x-0\.5[data-state=unchecked]{--tw-translate-x:calc(var(--spacing) * .5);translate:var(--tw-translate-x) var(--tw-translate-y)}.data-\[state\=unchecked\]\:bg-neutral-500[data-state=unchecked]{background-color:var(--color-neutral-500)}.data-\[state\=unchecked\]\:bg-transparent[data-state=unchecked]{background-color:#0000}@media (width>=40rem){.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (width>=48rem){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (width>=64rem){.lg\:grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.lg\:grid-cols-\[minmax\(0\,1fr\)_260px\]{grid-template-columns:minmax(0,1fr) 260px}.lg\:grid-cols-\[minmax\(0\,1fr\)_300px\]{grid-template-columns:minmax(0,1fr) 300px}.lg\:grid-cols-\[minmax\(0\,1fr\)_340px\]{grid-template-columns:minmax(0,1fr) 340px}.lg\:grid-cols-\[minmax\(0\,7fr\)_minmax\(0\,5fr\)\]{grid-template-columns:minmax(0,7fr) minmax(0,5fr)}}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}.\[\&\>svg\]\:size-3>svg{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}}@property --tw-border-spacing-x{syntax:"<length>";inherits:false;initial-value:0}@property --tw-border-spacing-y{syntax:"<length>";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@keyframes pulse{50%{opacity:.5}}
@@ -0,0 +1,19 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>PAL control room</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ rel="stylesheet"
11
+ href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600&family=Barlow+Condensed:wght@400;500;600&display=swap"
12
+ />
13
+ <script type="module" crossorigin src="/assets/index-BoQjFpzV.js"></script>
14
+ <link rel="stylesheet" crossorigin href="/assets/index-Dncp2bYg.css">
15
+ </head>
16
+ <body>
17
+ <div id="root"></div>
18
+ </body>
19
+ </html>
@@ -6,10 +6,6 @@ export function age(days: number): string {
6
6
  return `${Math.floor(days / 30)}mo`;
7
7
  }
8
8
 
9
- export function plural(n: number, one: string, many = `${one}s`): string {
10
- return `${n} ${n === 1 ? one : many}`;
11
- }
12
-
13
9
  export function clock(iso: string): string {
14
10
  const at = new Date(iso);
15
11
  if (Number.isNaN(at.getTime())) return iso;
@@ -18,14 +14,6 @@ export function clock(iso: string): string {
18
14
  return `${day} ${time}`;
19
15
  }
20
16
 
21
- export function isoDay(at: Date): string {
22
- return at.toISOString().slice(0, 10);
23
- }
24
-
25
17
  export function tenths(n: number): string {
26
18
  return n.toFixed(1);
27
19
  }
28
-
29
- export function percent(alreadyPercent: number): string {
30
- return `${Math.round(alreadyPercent)}%`;
31
- }
@@ -0,0 +1,125 @@
1
+ import type { ReactNode } from "react";
2
+ import { Card, CardAction, CardContent, CardHeader, CardTitle } from "./components/card";
3
+ import { Skeleton } from "./components/skeleton";
4
+ import { ToggleGroup, ToggleGroupItem } from "./components/toggle-group";
5
+ import type { Loaded } from "./lib/api";
6
+ import { cn } from "./lib/cn";
7
+
8
+ export function Panel({
9
+ title,
10
+ aside,
11
+ className,
12
+ children,
13
+ }: {
14
+ title?: string;
15
+ aside?: ReactNode;
16
+ className?: string;
17
+ children: ReactNode;
18
+ }) {
19
+ return (
20
+ <Card className={className}>
21
+ {title && (
22
+ <CardHeader>
23
+ <CardTitle>{title}</CardTitle>
24
+ {aside && <CardAction>{aside}</CardAction>}
25
+ </CardHeader>
26
+ )}
27
+ <CardContent>{children}</CardContent>
28
+ </Card>
29
+ );
30
+ }
31
+
32
+ export interface SegOption<T extends string> {
33
+ value: T;
34
+ label: string;
35
+ }
36
+
37
+ export function Seg<T extends string>({
38
+ options,
39
+ value,
40
+ onPick,
41
+ label,
42
+ }: {
43
+ options: readonly SegOption<T>[];
44
+ value: T;
45
+ onPick: (next: T) => void;
46
+ label: string;
47
+ }) {
48
+ return (
49
+ <ToggleGroup
50
+ type="single"
51
+ aria-label={label}
52
+ value={value}
53
+ onValueChange={(next) => next && onPick(next as T)}
54
+ >
55
+ {options.map((option) => (
56
+ <ToggleGroupItem key={option.value} value={option.value}>
57
+ {option.label}
58
+ </ToggleGroupItem>
59
+ ))}
60
+ </ToggleGroup>
61
+ );
62
+ }
63
+
64
+ /**
65
+ * A screen whose frame stays put: the title and whatever toolbar it carries
66
+ * hold their place, and only `children` scrolls. Children are handed a region
67
+ * that is already the right height, so a table inside it scrolls its own rows
68
+ * rather than the window.
69
+ */
70
+ export function Screen({
71
+ title,
72
+ aside,
73
+ toolbar,
74
+ children,
75
+ }: {
76
+ title: string;
77
+ aside?: ReactNode;
78
+ toolbar?: ReactNode;
79
+ children: ReactNode;
80
+ }) {
81
+ return (
82
+ <div className="flex min-h-0 flex-1 flex-col">
83
+ <div className="mb-4 flex flex-wrap items-baseline justify-between gap-3">
84
+ <h1 className="m-0 text-[34px]">{title}</h1>
85
+ {aside}
86
+ </div>
87
+ {toolbar}
88
+ <div className="flex min-h-0 flex-1 flex-col">{children}</div>
89
+ </div>
90
+ );
91
+ }
92
+
93
+ /** The scroll region itself — one per screen, and the only thing that moves. */
94
+ export function Scroller({
95
+ className,
96
+ children,
97
+ }: {
98
+ className?: string;
99
+ children: ReactNode;
100
+ }) {
101
+ return <div className={cn("min-h-0 flex-1 overflow-auto", className)}>{children}</div>;
102
+ }
103
+
104
+ export function Pending({ value }: { value: Loaded<unknown> }) {
105
+ if (value.state === "loading")
106
+ return (
107
+ <div className="flex flex-col gap-1.5 py-2">
108
+ <span className="sr-only">reading</span>
109
+ <Skeleton className="h-3 w-2/5" />
110
+ <Skeleton className="h-3 w-3/5" />
111
+ <Skeleton className="h-3 w-1/3" />
112
+ </div>
113
+ );
114
+ if (value.state === "error")
115
+ return (
116
+ <div className="border-l-2 border-alarm bg-alarm/10 px-3 py-2 text-[12px] text-alarm">
117
+ {value.message}
118
+ </div>
119
+ );
120
+ return null;
121
+ }
122
+
123
+ export function Empty({ children }: { children: ReactNode }) {
124
+ return <div className="py-2 text-[12px] text-neutral-500">{children}</div>;
125
+ }
@@ -8,9 +8,9 @@
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
9
  <link
10
10
  rel="stylesheet"
11
- href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;1,6..72,400;1,6..72,500&family=Martian+Mono:wght@300;400;600&display=swap"
11
+ href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600&family=Barlow+Condensed:wght@400;500;600&display=swap"
12
12
  />
13
- <link rel="stylesheet" href="./app.css" />
13
+ <link rel="stylesheet" href="./theme.css" />
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>
@@ -0,0 +1,27 @@
1
+ import { useEffect, useState } from "react";
2
+
3
+ export type Loaded<T> =
4
+ | { state: "loading" }
5
+ | { state: "error"; message: string }
6
+ | { state: "ready"; data: T };
7
+
8
+ async function getJson<T>(path: string): Promise<T> {
9
+ const res = await fetch(path);
10
+ if (!res.ok) throw new Error(`${path} answered ${res.status}`);
11
+ return (await res.json()) as T;
12
+ }
13
+
14
+ export function useLoaded<T>(url: string): Loaded<T> {
15
+ const [value, setValue] = useState<Loaded<T>>({ state: "loading" });
16
+ useEffect(() => {
17
+ let live = true;
18
+ setValue({ state: "loading" });
19
+ getJson<T>(url)
20
+ .then((data) => live && setValue({ state: "ready", data }))
21
+ .catch((e: Error) => live && setValue({ state: "error", message: e.message }));
22
+ return () => {
23
+ live = false;
24
+ };
25
+ }, [url]);
26
+ return value;
27
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The page's four write calls. Each POSTs one named field and hands back the
3
+ * server's own error text, so a refusal reads the same wherever it surfaces.
4
+ */
5
+
6
+ export async function post(path: string, body: unknown): Promise<string | null> {
7
+ try {
8
+ const res = await fetch(path, {
9
+ method: "POST",
10
+ headers: { "content-type": "application/json" },
11
+ body: JSON.stringify(body),
12
+ });
13
+ if (res.ok) return null;
14
+ const payload = (await res.json().catch(() => null)) as { error?: string } | null;
15
+ return payload?.error ?? `${path} answered ${res.status}`;
16
+ } catch (e) {
17
+ return e instanceof Error ? e.message : String(e);
18
+ }
19
+ }
20
+
21
+ export function setIsc(project: string, id: number, status: "open" | "done") {
22
+ return post("/api/isc", { project, id, status });
23
+ }
24
+
25
+ export function setPlacement(project: string, quadrant: string | null) {
26
+ return post("/api/placement", { project, quadrant });
27
+ }
28
+
29
+ export function setServes(project: string, serves: string) {
30
+ return post("/api/serves", { project, serves });
31
+ }
32
+
33
+ export function setInstallSettings(update: Record<string, string>) {
34
+ return post("/api/settings", update);
35
+ }
36
+
37
+ export function setSnooze(project: string, id: number, days: number) {
38
+ return post("/api/snooze", { project, id, days });
39
+ }
40
+
41
+ export function setPrefs(update: Record<string, unknown>) {
42
+ return post("/api/prefs", update);
43
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@pal/control-room-ui",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "The control room's page. Built here, served by the PAL CLI, never installed by a PAL user — the published package carries dist/, not these dependencies.",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "vite build",
9
+ "dev": "vite"
10
+ },
11
+ "dependencies": {
12
+ "class-variance-authority": "0.7.1",
13
+ "clsx": "2.1.1",
14
+ "radix-ui": "1.6.7",
15
+ "react": "^19.2.8",
16
+ "react-dom": "^19.2.8",
17
+ "react-router": "8.3.1",
18
+ "tailwind-merge": "3.6.0"
19
+ },
20
+ "devDependencies": {
21
+ "@tailwindcss/vite": "4.3.3",
22
+ "@types/react": "^19.2.18",
23
+ "@types/react-dom": "^19.2.7",
24
+ "@vitejs/plugin-react": "6.1.1",
25
+ "tailwindcss": "4.3.3",
26
+ "vite": "8.2.2"
27
+ }
28
+ }
@@ -0,0 +1,235 @@
1
+ import { Link } from "react-router";
2
+ import type { AgendaMove } from "../../../hooks/lib/agenda-store";
3
+ import type { HandoffCard } from "../data";
4
+ import type { MatrixItem } from "../matrix";
5
+ import { Badge } from "./components/badge";
6
+ import { NativeSelect } from "./components/input";
7
+ import { age } from "./format";
8
+ import { Empty } from "./frame";
9
+ import { cn } from "./lib/cn";
10
+ import { setPlacement, setServes } from "./lib/write";
11
+
12
+ const SERVES_KINDS = ["goal", "revenue", "fun"] as const;
13
+
14
+ function ServesSelect({ item, onSaved }: { item: MatrixItem; onSaved: () => void }) {
15
+ if (item.kind !== "project") return null;
16
+ return (
17
+ <NativeSelect
18
+ aria-label={`what ${item.label} serves`}
19
+ value={item.serves ?? ""}
20
+ title={
21
+ item.servesBy === "user" ? "your answer" : "PAL's guess — change it to correct it"
22
+ }
23
+ onChange={(e) => {
24
+ void setServes(item.id, e.target.value).then(onSaved);
25
+ }}
26
+ className={cn(
27
+ "h-auto py-0.5 text-[10px]",
28
+ item.servesBy === "user" && "border-accent bg-accent-100 text-accent-900"
29
+ )}
30
+ >
31
+ <option value="" disabled>
32
+ unranked
33
+ </option>
34
+ {SERVES_KINDS.map((kind) => (
35
+ <option key={kind} value={kind}>
36
+ {kind}
37
+ </option>
38
+ ))}
39
+ </NativeSelect>
40
+ );
41
+ }
42
+
43
+ const PLACEMENTS = [
44
+ { value: "", label: "let PAL place it" },
45
+ { value: "now", label: "Do now" },
46
+ { value: "plan", label: "Give it a slot" },
47
+ { value: "noise", label: "Loud, not load-bearing" },
48
+ { value: "later", label: "Let it sit" },
49
+ ] as const;
50
+
51
+ /** Overrules the grid's own reading. The correction survives the next guess. */
52
+ function PlacementSelect({ item, onSaved }: { item: MatrixItem; onSaved: () => void }) {
53
+ if (item.kind !== "project") return null;
54
+ return (
55
+ <NativeSelect
56
+ aria-label={`where ${item.label} belongs`}
57
+ value={item.placed ?? ""}
58
+ title={item.placed ? "your placement" : "placed by PAL — change it to overrule"}
59
+ onChange={(e) => {
60
+ void setPlacement(item.id, e.target.value || null).then(onSaved);
61
+ }}
62
+ className={cn(
63
+ "h-auto py-0.5 text-[10px]",
64
+ item.placed && "border-accent bg-accent-100 text-accent-900"
65
+ )}
66
+ >
67
+ {PLACEMENTS.map((p) => (
68
+ <option key={p.value} value={p.value}>
69
+ {p.label}
70
+ </option>
71
+ ))}
72
+ </NativeSelect>
73
+ );
74
+ }
75
+
76
+ export function ReasonTags({ item }: { item: MatrixItem }) {
77
+ return (
78
+ <div className="flex flex-wrap gap-1">
79
+ {item.urgentBecause.map((reason) => (
80
+ <Badge key={reason} variant="accent">
81
+ {reason}
82
+ </Badge>
83
+ ))}
84
+ <Badge variant="neutral">{item.importantBecause}</Badge>
85
+ </div>
86
+ );
87
+ }
88
+
89
+ export function ItemCard({ item, onSaved }: { item: MatrixItem; onSaved: () => void }) {
90
+ const isProject = item.kind === "project";
91
+ return (
92
+ <article className="flex flex-col gap-2 border border-divider bg-bg px-3 py-3 hover:border-accent">
93
+ <div className="flex items-center gap-2">
94
+ {isProject ? (
95
+ <Link
96
+ to={`/projects/${item.id}`}
97
+ className="font-heading text-[16px] font-semibold text-ink hover:text-accent"
98
+ >
99
+ {item.label}
100
+ </Link>
101
+ ) : (
102
+ <span className="font-heading text-[16px] font-semibold text-ink">
103
+ {item.label}
104
+ </span>
105
+ )}
106
+ {isProject ? (
107
+ <ServesSelect item={item} onSaved={onSaved} />
108
+ ) : (
109
+ <Badge variant="accent">goal</Badge>
110
+ )}
111
+ <span className="ml-auto text-[11px] whitespace-nowrap text-neutral-600">
112
+ {item.due ?? ""}
113
+ </span>
114
+ </div>
115
+ {item.waitingOn && (
116
+ <p className="bg-accent-100 px-2 py-1.5 text-[12px] text-accent-900">
117
+ waiting on you · {item.waitingOn}
118
+ </p>
119
+ )}
120
+ {item.detail && <p className="text-[12px] text-neutral-800">{item.detail}</p>}
121
+ <ReasonTags item={item} />
122
+ {isProject && (
123
+ <div className="flex items-center gap-2 border-t border-divider/60 pt-2">
124
+ <span className="eyebrow">placed</span>
125
+ <PlacementSelect item={item} onSaved={onSaved} />
126
+ </div>
127
+ )}
128
+ </article>
129
+ );
130
+ }
131
+
132
+ export function MovesList({
133
+ moves,
134
+ size = "compact",
135
+ }: {
136
+ moves: AgendaMove[];
137
+ size?: "compact" | "large";
138
+ }) {
139
+ if (moves.length === 0) {
140
+ return (
141
+ <Empty>
142
+ No moves yet. They are written when a session ends — finish one and come back.
143
+ </Empty>
144
+ );
145
+ }
146
+ return (
147
+ <ol className="flex list-none flex-col gap-0 p-0">
148
+ {moves.map((move, i) => (
149
+ <li
150
+ key={move.move}
151
+ className={cn(
152
+ "grid items-start gap-4 border-t border-divider py-4 first:border-t-0",
153
+ size === "large" ? "grid-cols-[44px_1fr]" : "grid-cols-[26px_1fr]"
154
+ )}
155
+ >
156
+ <span
157
+ className={cn(
158
+ "font-heading leading-none font-semibold text-accent",
159
+ size === "large" ? "text-[40px]" : "text-[22px]"
160
+ )}
161
+ >
162
+ {String(i + 1).padStart(2, "0")}
163
+ </span>
164
+ <span>
165
+ <span
166
+ className={cn(
167
+ "block font-medium text-pretty",
168
+ size === "large" && "font-heading text-[22px] leading-tight font-semibold"
169
+ )}
170
+ >
171
+ {move.move}
172
+ </span>
173
+ <span className="text-[12px] text-neutral-700">{move.because}</span>
174
+ </span>
175
+ </li>
176
+ ))}
177
+ </ol>
178
+ );
179
+ }
180
+
181
+ export function HandoffList({ cards }: { cards: HandoffCard[] }) {
182
+ if (cards.length === 0) {
183
+ return <Empty>Nothing in progress. Every handoff is closed.</Empty>;
184
+ }
185
+ return (
186
+ <div className="flex flex-col gap-3">
187
+ {cards.map((h) => (
188
+ <div key={h.cwd} className="flex flex-col gap-1">
189
+ <span className="flex justify-between gap-2 text-[11px] text-neutral-600">
190
+ {h.slug ? (
191
+ <Link
192
+ to={`/projects/${h.slug}`}
193
+ className="font-semibold text-ink hover:text-accent"
194
+ >
195
+ {h.label}
196
+ </Link>
197
+ ) : (
198
+ <b className="font-semibold text-ink">{h.label}</b>
199
+ )}
200
+ <span>
201
+ {h.source} · {age(h.ageDays)}
202
+ </span>
203
+ </span>
204
+ <span className="text-[12.5px] text-pretty" title={h.handoff}>
205
+ {h.sentence}
206
+ </span>
207
+ </div>
208
+ ))}
209
+ </div>
210
+ );
211
+ }
212
+
213
+ export function GoalsList({ goals }: { goals: MatrixItem[] }) {
214
+ if (goals.length === 0) return <Empty>No goals written down yet.</Empty>;
215
+ return (
216
+ <div className="flex flex-col gap-3">
217
+ {goals.map((goal) => (
218
+ <div key={goal.id}>
219
+ <div className="flex justify-between gap-2 text-[12.5px]">
220
+ <span className="text-pretty">{goal.label}</span>
221
+ <span className="text-[11px] whitespace-nowrap text-neutral-600">
222
+ {goal.due ?? goal.detail}
223
+ </span>
224
+ </div>
225
+ <div className="mt-1 text-[11px] text-neutral-600">
226
+ {goal.progress
227
+ ? `${goal.progress.closed} of ${goal.progress.written} criteria closed · served by ${goal.progress.projects.join(", ")}`
228
+ : "no project serves this yet"}
229
+ {goal.urgentBecause.length > 0 && ` · ${goal.urgentBecause.join(" · ")}`}
230
+ </div>
231
+ </div>
232
+ ))}
233
+ </div>
234
+ );
235
+ }