Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Pricing says it plainly: models at the provider's price, the agent rate for what g1t runs around every model call (the gateway, secrets, routing, context and pass-through to your own provider, so your own keys too), everything else at cost plus 20%, your own runners free, no seats; Security and quality comes with the plan with no separate fee, and live activations end. Each agent has an effort setting, Auto to Max, with what a typical task has cost at each level, and Spend's Spend less, keep quality suggests a lower level only when the agent's own past work shows quality held, to apply or dismiss. The pricing, spend and agents guides say how. | 1 | /** |
| 2 | * Effort and "spend less, keep quality": the control that sets how hard an | |
| 3 | * agent works, with what each level has cost it, and the weekly | |
| 4 | * suggestions with Apply and Dismiss. Every figure is the agents service's, | |
| 5 | * measured from the agent's own sessions (lib/effort.ts). | |
| 6 | */ | |
| 7 | import { Check, Gauge, Lightbulb } from "lucide-react"; | |
| 8 | import { useState } from "react"; | |
| 9 | import { useFetcher } from "react-router"; | |
| 10 | ||
| 11 | import type { AgentEffort, AgentEffortCosts, AgentRecommendation, AgentRecommendations } from "@g1t/contracts"; | |
| 12 | ||
| 13 | import { AgentAvatar } from "./agent-avatar"; | |
| 14 | import { BUTTONS } from "./agents/dialogs"; | |
| 15 | import { Hint } from "./ui/hint"; | |
| 16 | import { TimeAgo } from "./ui"; | |
| 17 | import { cn } from "../lib/cn"; | |
| 18 | import { EFFORT_OPTIONS, acceptedLabel, costAt, costLine, effortLabel, savingLabel, totalSaving } from "../lib/effort"; | |
| 19 | import { money } from "../lib/usage"; | |
| 20 | ||
| 21 | /** | |
| 22 | * The five settings as one segmented control, radio inputs underneath so | |
| 23 | * it works inside any form. Each has a hint saying what it does; under it, | |
| 24 | * what the chosen one has cost this agent. | |
| 25 | */ | |
| 26 | export function EffortPicker({ | |
| 27 | name = "effort", | |
| 28 | value, | |
| 29 | costs, | |
| 30 | disabled = false, | |
| 31 | onChange, | |
| 32 | }: { | |
| 33 | name?: string; | |
| 34 | value: AgentEffort; | |
| 35 | costs: AgentEffortCosts | null | undefined; | |
| 36 | disabled?: boolean; | |
| 37 | onChange?: (effort: AgentEffort) => void; | |
| 38 | }) { | |
| 39 | const [chosen, setChosen] = useState<AgentEffort>(value); | |
| 40 | return ( | |
| 41 | <div className="min-w-0"> | |
| 42 | <div role="radiogroup" aria-label="Effort" className="grid grid-cols-5 gap-0.5 rounded-lg border border-line bg-bg p-0.5 sm:inline-grid sm:w-auto sm:min-w-[22rem]"> | |
| 43 | {EFFORT_OPTIONS.map((option) => { | |
| 44 | const on = chosen === option.key; | |
| 45 | return ( | |
| 46 | <Hint key={option.key} label={option.about}> | |
| 47 | <label | |
| 48 | className={cn( | |
| 49 | "relative flex h-8 cursor-pointer items-center justify-center rounded-md px-2 text-[0.8125rem] transition-colors select-none has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-accent", | |
| 50 | on ? "bg-raised font-medium text-fg ring-1 ring-line-strong" : "text-muted hover:text-fg", | |
| 51 | disabled && "cursor-not-allowed opacity-60", | |
| 52 | )} | |
| 53 | > | |
| 54 | <input | |
| 55 | type="radio" | |
| 56 | name={name} | |
| 57 | value={option.key} | |
| 58 | checked={on} | |
| 59 | disabled={disabled} | |
| 60 | onChange={() => { | |
| 61 | setChosen(option.key); | |
| 62 | onChange?.(option.key); | |
| 63 | }} | |
| 64 | className="sr-only" | |
| 65 | /> | |
| 66 | {option.label} | |
| 67 | </label> | |
| 68 | </Hint> | |
| 69 | ); | |
| 70 | })} | |
| 71 | </div> | |
| 72 | <p className="mt-2 text-xs text-muted">{EFFORT_OPTIONS.find((o) => o.key === chosen)?.about}</p> | |
| 73 | {costs !== undefined && <p className="mt-0.5 text-xs text-faint tabular-nums">{costLine(costs, chosen)}</p>} | |
| 74 | </div> | |
| 75 | ); | |
| 76 | } | |
| 77 | ||
| 78 | /** What each level has cost this agent: a typical task, how many sessions, and how often nobody had to step in. */ | |
| 79 | export function EffortCosts({ costs }: { costs: AgentEffortCosts | null }) { | |
| 80 | if (!costs) return null; | |
| 81 | return ( | |
| 82 | <table className="w-full text-left text-xs"> | |
| 83 | <caption className="sr-only">What each effort level has cost, from this agent's sessions</caption> | |
| 84 | <thead className="text-faint"> | |
| 85 | <tr> | |
| 86 | <th className="py-1.5 pr-3 font-normal">Level</th> | |
| 87 | <th className="py-1.5 pr-3 font-normal">Typical task</th> | |
| 88 | <th className="py-1.5 pr-3 font-normal">Sessions</th> | |
| 89 | <th className="py-1.5 font-normal"> | |
| 90 | <Hint label="Finished with nobody having to step in: not steered, stopped or failed."> | |
| 91 | <span className="cursor-help underline decoration-dotted underline-offset-2" tabIndex={0}> | |
| 92 | Accepted | |
| 93 | </span> | |
| 94 | </Hint> | |
| 95 | </th> | |
| 96 | </tr> | |
| 97 | </thead> | |
| 98 | <tbody className="divide-y divide-line/60 tabular-nums"> | |
| 99 | {costs.levels.map((level) => { | |
| 100 | const at = costAt(costs, level.effort); | |
| 101 | return ( | |
| 102 | <tr key={level.effort} className={cn(costs.effort === level.effort && "text-fg")}> | |
| 103 | <td className="py-1.5 pr-3"> | |
| 104 | {effortLabel(level.effort)} | |
| 105 | {costs.effort === level.effort && <span className="ml-1.5 text-faint">now</span>} | |
| 106 | </td> | |
| 107 | <td className="py-1.5 pr-3">{at ? money(at.typical_micros!) : <span className="text-faint">Not measured</span>}</td> | |
| 108 | <td className="py-1.5 pr-3 text-muted">{level.sessions}</td> | |
| 109 | <td className="py-1.5 text-muted">{acceptedLabel(at) || "—"}</td> | |
| 110 | </tr> | |
| 111 | ); | |
| 112 | })} | |
| 113 | </tbody> | |
| 114 | </table> | |
| 115 | ); | |
| 116 | } | |
| 117 | ||
| 118 | /** | |
| 119 | * One agent's effort, on its Spend tab: the control for owners (saved on | |
| 120 | * its own), read-only for everyone else, and what each level has cost it. | |
| 121 | */ | |
| 122 | export function EffortCard({ value, costs, owner, action, name }: { value: AgentEffort; costs: AgentEffortCosts | null; owner: boolean; action: string; name: string }) { | |
| 123 | const fetcher = useFetcher<{ ok: boolean; intent?: string; error?: string }>(); | |
| 124 | const [picked, setPicked] = useState<AgentEffort>(value); | |
| 125 | const busy = fetcher.state !== "idle"; | |
| 126 | const result = fetcher.data?.intent === "effort" ? fetcher.data : null; | |
| 127 | return ( | |
| 128 | <section className="rounded-2xl border border-line bg-surface p-5 sm:p-6"> | |
| 129 | <div className="flex items-start gap-3"> | |
| 130 | <span className="mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg bg-accent/15 text-accent"> | |
| 131 | <Gauge size={16} aria-hidden="true" /> | |
| 132 | </span> | |
| 133 | <div className="min-w-0"> | |
| 134 | <h2 className="text-sm font-semibold">Effort</h2> | |
| 135 | <p className="mt-0.5 text-[0.8125rem] leading-relaxed text-muted"> | |
| 136 | How hard {name} works: the tier its work starts on, how hard the model reasons, and how many steps a session may take. Its floor and | |
| 137 | ceiling still hold. | |
| 138 | </p> | |
| 139 | </div> | |
| 140 | </div> | |
| 141 | <fetcher.Form method="post" action={action} className="mt-4 space-y-3"> | |
| 142 | <input type="hidden" name="intent" value="effort" /> | |
| 143 | <EffortPicker value={value} costs={costs} disabled={!owner} onChange={setPicked} /> | |
| 144 | {owner && ( | |
| 145 | <div className="flex flex-wrap items-center gap-3"> | |
| 146 | <button type="submit" disabled={busy || picked === value} className={BUTTONS.PRIMARY}> | |
| 147 | {busy ? "Saving…" : "Save effort"} | |
| 148 | </button> | |
| 149 | {result?.ok && picked === value && ( | |
| 150 | <span role="status" className="inline-flex items-center gap-1 text-xs text-success"> | |
| 151 | <Check size={13} aria-hidden="true" /> Saved as a new version | |
| 152 | </span> | |
| 153 | )} | |
| 154 | {result && !result.ok && <span className="text-xs text-danger">{result.error}</span>} | |
| 155 | </div> | |
| 156 | )} | |
| 157 | {!owner && <p className="text-xs text-faint">Only the workspace's owners change it.</p>} | |
| 158 | </fetcher.Form> | |
| 159 | <div className="mt-5 border-t border-line pt-4"> | |
| 160 | <p className="mb-1 text-xs font-medium text-muted">What each level has cost {name}{costs ? `, last ${costs.window_days} days` : ""}</p> | |
| 161 | {costs ? <EffortCosts costs={costs} /> : <p className="text-xs text-faint">Costs couldn't be read right now.</p>} | |
| 162 | </div> | |
| 163 | </section> | |
| 164 | ); | |
| 165 | } | |
| 166 | ||
| 167 | /** | |
| 168 | * Spend less, keep quality: the weekly check's suggestions, each with the | |
| 169 | * evidence it rests on and Apply / Dismiss for owners; agents with too | |
| 170 | * little history to say, said plainly; and what was decided lately. | |
| 171 | */ | |
| 172 | export function Savings({ recs, owner, action, showAgent = true }: { recs: AgentRecommendations | null; owner: boolean; action: string; showAgent?: boolean }) { | |
| 173 | if (!recs) return <p className="rounded-xl border border-line bg-surface px-4 py-6 text-center text-sm text-muted">Suggestions couldn't be read right now.</p>; | |
| 174 | const total = totalSaving(recs); | |
| 175 | return ( | |
| 176 | <div className="overflow-hidden rounded-xl border border-accent/30 bg-surface"> | |
| 177 | <div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1 border-b border-line bg-accent/[0.05] px-4 py-3"> | |
| 178 | <p className="flex items-center gap-2 text-sm font-medium"> | |
| 179 | <Lightbulb size={14} className="text-accent" aria-hidden="true" /> | |
| 180 | {recs.open.length ? `${recs.open.length} suggestion${recs.open.length === 1 ? "" : "s"}` : "No suggestions right now"} | |
| 181 | {total > 0 && <span className="font-normal text-success tabular-nums">· about {money(total)} a month</span>} | |
| 182 | </p> | |
| 183 | <p className="text-xs text-faint"> | |
| 184 | {recs.checked_at ? ( | |
| 185 | <> | |
| 186 | Checked against your own past work <TimeAgo at={recs.checked_at} />, weekly | |
| 187 | </> | |
| 188 | ) : ( | |
| 189 | "Checked weekly against your agents' own finished sessions" | |
| 190 | )} | |
| 191 | </p> | |
| 192 | </div> | |
| 193 | {recs.open.length > 0 && ( | |
| 194 | <ul className="divide-y divide-line"> | |
| 195 | {recs.open.map((rec) => ( | |
| 196 | <Suggestion key={rec.id} rec={rec} owner={owner} action={action} showAgent={showAgent} /> | |
| 197 | ))} | |
| 198 | </ul> | |
| 199 | )} | |
| 200 | {recs.open.length === 0 && ( | |
| 201 | <p className="px-4 py-4 text-sm text-muted"> | |
| 202 | {recs.checked_at | |
| 203 | ? "Nothing measured would save money without the work getting worse." | |
| 204 | : "The first check runs once agents have finished sessions. A suggestion appears only when a cheaper level held up on the same agent's past work."} | |
| 205 | </p> | |
| 206 | )} | |
| 207 | {recs.thin.length > 0 && ( | |
| 208 | <div className="border-t border-line px-4 py-3"> | |
| 209 | <p className="text-xs font-medium text-muted">Not enough history yet</p> | |
| 210 | <ul className="mt-1.5 space-y-1.5"> | |
| 211 | {recs.thin.map((rec) => ( | |
| 212 | <li key={rec.id} className="flex min-w-0 items-start gap-2 text-xs text-faint"> | |
| 213 | {showAgent && <AgentAvatar agent={{ handle: rec.agent_handle, avatar_seed: rec.agent_avatar_seed }} size={16} />} | |
| 214 | <span className="min-w-0"> | |
| 215 | {showAgent && <span className="text-muted">@{rec.agent_handle}: </span>} | |
| 216 | {rec.reason} | |
| 217 | </span> | |
| 218 | </li> | |
| 219 | ))} | |
| 220 | </ul> | |
| 221 | </div> | |
| 222 | )} | |
| 223 | {recs.resolved.length > 0 && ( | |
| 224 | <div className="border-t border-line px-4 py-3"> | |
| 225 | <p className="text-xs font-medium text-muted">Decided lately</p> | |
| 226 | <ul className="mt-1.5 space-y-1"> | |
| 227 | {recs.resolved.slice(0, 5).map((rec) => ( | |
| 228 | <li key={rec.id} className="truncate text-xs text-faint"> | |
| 229 | {rec.status === "applied" ? "Applied" : "Dismissed"}: {rec.title} | |
| 230 | {rec.resolved_by ? ` · @${rec.resolved_by}` : ""} | |
| 231 | {rec.resolved_at ? ( | |
| 232 | <> | |
| 233 | {" "} | |
| 234 | · <TimeAgo at={rec.resolved_at} /> | |
| 235 | </> | |
| 236 | ) : null} | |
| 237 | </li> | |
| 238 | ))} | |
| 239 | </ul> | |
| 240 | </div> | |
| 241 | )} | |
| 242 | </div> | |
| 243 | ); | |
| 244 | } | |
| 245 | ||
| 246 | function Suggestion({ rec, owner, action, showAgent }: { rec: AgentRecommendation; owner: boolean; action: string; showAgent: boolean }) { | |
| 247 | const fetcher = useFetcher<{ ok: boolean; intent?: string; error?: string }>(); | |
| 248 | const doing = fetcher.formData?.get("do"); | |
| 249 | const error = fetcher.data && !fetcher.data.ok && fetcher.data.intent === "recommendation" ? fetcher.data.error : null; | |
| 250 | const saving = savingLabel(rec); | |
| 251 | return ( | |
| 252 | <li className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 gap-y-2 px-4 py-3.5 sm:grid-cols-[auto_minmax(0,1fr)_auto]"> | |
| 253 | <span className="pt-0.5"> | |
| 254 | {showAgent ? <AgentAvatar agent={{ handle: rec.agent_handle, avatar_seed: rec.agent_avatar_seed }} size={26} /> : <Gauge size={18} className="text-accent" aria-hidden="true" />} | |
| 255 | </span> | |
| 256 | <div className="min-w-0"> | |
| 257 | <p className="text-sm font-medium">{rec.title}</p> | |
| 258 | <p className="mt-0.5 text-[0.8125rem] leading-relaxed text-muted">{rec.reason}</p> | |
| 259 | <p className="mt-1 text-xs text-faint"> | |
| 260 | {effortLabel(rec.from_effort)} to {effortLabel(rec.to_effort)} | |
| 261 | {saving && <span className="text-success"> · {saving}</span>} | |
| 262 | </p> | |
| 263 | {error && <p className="mt-1 text-xs text-danger">{error}</p>} | |
| 264 | </div> | |
| 265 | {owner ? ( | |
| 266 | <fetcher.Form method="post" action={action} className="col-span-2 flex items-center gap-2 sm:col-span-1 sm:self-center"> | |
| 267 | <input type="hidden" name="intent" value="recommendation" /> | |
| 268 | <input type="hidden" name="id" value={rec.id} /> | |
| 269 | <button type="submit" name="do" value="apply" disabled={fetcher.state !== "idle"} className={cn(BUTTONS.PRIMARY, "h-8 px-3 py-0 text-xs")}> | |
| 270 | {doing === "apply" ? "Applying…" : "Apply"} | |
| 271 | </button> | |
| 272 | <button type="submit" name="do" value="dismiss" disabled={fetcher.state !== "idle"} className={cn(BUTTONS.QUIET, "h-8 px-3 py-0 text-xs")}> | |
| 273 | {doing === "dismiss" ? "Dismissing…" : "Dismiss"} | |
| 274 | </button> | |
| 275 | </fetcher.Form> | |
| 276 | ) : ( | |
| 277 | <p className="col-span-2 text-xs text-faint sm:col-span-1 sm:self-center">An owner can apply it</p> | |
| 278 | )} | |
| 279 | </li> | |
| 280 | ); | |
| 281 | } |