portable-agent-layer 0.107.0 → 0.109.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.
@@ -10,7 +10,7 @@
10
10
  rel="stylesheet"
11
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
- <script type="module" crossorigin src="/assets/index-mFpXpiBj.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-D0pEd7Ln.js"></script>
14
14
  <link rel="stylesheet" crossorigin href="/assets/index-DeglMbfS.css">
15
15
  </head>
16
16
  <body>
@@ -3,6 +3,8 @@
3
3
  * server's own error text, so a refusal reads the same wherever it surfaces.
4
4
  */
5
5
 
6
+ import type { PageDecision } from "../../relationship";
7
+
6
8
  export async function post(path: string, body: unknown): Promise<string | null> {
7
9
  try {
8
10
  const res = await fetch(path, {
@@ -46,7 +48,7 @@ export function setAutoUpdate(enabled: boolean) {
46
48
  return post("/api/update", { enabled });
47
49
  }
48
50
 
49
- export function decideRule(id: string, decision: "approved" | "denied") {
51
+ export function decideRule(id: string, decision: PageDecision) {
50
52
  return post("/api/rule", { id, decision });
51
53
  }
52
54
 
@@ -3,7 +3,14 @@ import type {
3
3
  AdaptationRule,
4
4
  TriggerProof,
5
5
  } from "../../../../hooks/lib/adaptation-rules";
6
- import type { ActiveRule, PipelineCandidate, RelationshipView } from "../../relationship";
6
+ import type { OpinionEffect } from "../../../../hooks/lib/correction-rates";
7
+ import type {
8
+ ActiveRule,
9
+ DraftRule,
10
+ PageDecision,
11
+ PipelineCandidate,
12
+ RelationshipView,
13
+ } from "../../relationship";
7
14
  import { Badge } from "../components/badge";
8
15
  import { Button } from "../components/button";
9
16
  import { clock } from "../format";
@@ -33,9 +40,14 @@ function Facts({ rows }: { rows: [string, string | undefined][] }) {
33
40
  );
34
41
  }
35
42
 
43
+ function triggerLine(rule: AdaptationRule): string {
44
+ if (rule.delivery === "always") return "none, an opinion steers every session";
45
+ return rule.trigger ? `${rule.trigger.side} · /${rule.trigger.pattern}/i` : "none";
46
+ }
47
+
36
48
  function ruleFacts(rule: AdaptationRule): [string, string | undefined][] {
37
49
  return [
38
- ["trigger", `${rule.trigger.side} · /${rule.trigger.pattern}/i`],
50
+ ["trigger", triggerLine(rule)],
39
51
  ["steering", rule.steering],
40
52
  ["check", rule.check],
41
53
  ["proof", proofLine(rule.proof)],
@@ -56,43 +68,82 @@ function Evidence({ items }: { items: string[] }) {
56
68
  function replacedTrigger(rule: AdaptationRule, active: ActiveRule[]): string | undefined {
57
69
  if (!rule.widens) return undefined;
58
70
  const target = active.find((r) => r.id === rule.widens);
59
- if (!target) return `the trigger of rule ${rule.widens}`;
71
+ if (!target?.trigger) return `the trigger of rule ${rule.widens}`;
60
72
  return `/${target.trigger.pattern}/i`;
61
73
  }
62
74
 
63
- function Draft({
64
- rule,
65
- active,
66
- onDecided,
67
- }: {
68
- rule: AdaptationRule;
69
- active: ActiveRule[];
70
- onDecided: () => void;
71
- }) {
75
+ function useDecision(id: string, onDecided: () => void) {
72
76
  const [error, setError] = useState<string | null>(null);
73
77
  const [busy, setBusy] = useState(false);
74
- const decide = (decision: "approved" | "denied") => {
78
+ const decide = (decision: PageDecision) => {
75
79
  setBusy(true);
76
- void decideRule(rule.id, decision).then((failure) => {
80
+ void decideRule(id, decision).then((failure) => {
77
81
  setBusy(false);
78
82
  setError(failure);
79
83
  if (!failure) onDecided();
80
84
  });
81
85
  };
86
+ return { error, busy, decide };
87
+ }
88
+
89
+ function Refusal({ error }: { error: string | null }) {
90
+ if (!error) return null;
91
+ return (
92
+ <p className="border-l-2 border-alarm bg-alarm/10 px-3 py-2 text-[12px] text-alarm">
93
+ {error}
94
+ </p>
95
+ );
96
+ }
97
+
98
+ function offeredDecisions(rule: AdaptationRule): [PageDecision, string][] {
99
+ if (rule.widens) return [["approved", "Approve"]];
100
+ const asOpinion: [PageDecision, string][] =
101
+ rule.kind && !rule.opinionGate ? [["opinion", "Keep as opinion"]] : [];
102
+ if (rule.offer === "opinion-only") return asOpinion;
103
+ return [["approved", "Approve as rule"], ...asOpinion];
104
+ }
105
+
106
+ function offerLine(rule: AdaptationRule): string | undefined {
107
+ if (rule.offer !== "opinion-only" || rule.opinionGate) return undefined;
108
+ const why = rule.trigger ? "the trigger did not prove" : "no trigger";
109
+ return `${why}; keep it as an opinion or deny it`;
110
+ }
111
+
112
+ function Draft({
113
+ rule,
114
+ active,
115
+ onDecided,
116
+ }: {
117
+ rule: DraftRule;
118
+ active: ActiveRule[];
119
+ onDecided: () => void;
120
+ }) {
121
+ const { error, busy, decide } = useDecision(rule.id, onDecided);
82
122
  return (
83
123
  <div className="flex flex-col gap-3 border-t border-divider py-3 first:border-t-0">
84
124
  <div className="font-heading text-[15px] font-semibold">{rule.when}</div>
85
- <Facts rows={[["replaces", replacedTrigger(rule, active)], ...ruleFacts(rule)]} />
125
+ <Facts
126
+ rows={[
127
+ ["replaces", replacedTrigger(rule, active)],
128
+ ...ruleFacts(rule),
129
+ ["kind", rule.kind],
130
+ ["offer", offerLine(rule)],
131
+ ["no opinion", rule.gateAdvice],
132
+ ]}
133
+ />
86
134
  <Evidence items={rule.evidence} />
87
- {error && (
88
- <p className="border-l-2 border-alarm bg-alarm/10 px-3 py-2 text-[12px] text-alarm">
89
- {error}
90
- </p>
91
- )}
92
- <div className="flex gap-3">
93
- <Button variant="primary" disabled={busy} onClick={() => decide("approved")}>
94
- Approve
95
- </Button>
135
+ <Refusal error={error} />
136
+ <div className="flex flex-wrap gap-3">
137
+ {offeredDecisions(rule).map(([decision, label], i) => (
138
+ <Button
139
+ key={decision}
140
+ variant={i === 0 ? "primary" : undefined}
141
+ disabled={busy}
142
+ onClick={() => decide(decision)}
143
+ >
144
+ {label}
145
+ </Button>
146
+ ))}
96
147
  <Button disabled={busy} onClick={() => decide("denied")}>
97
148
  Deny
98
149
  </Button>
@@ -101,18 +152,39 @@ function Draft({
101
152
  );
102
153
  }
103
154
 
104
- function effectLine(rule: ActiveRule): string {
155
+ function firingLine(rule: ActiveRule): string {
105
156
  const { fired, sentBack, judged, correctedAfter } = rule.effect;
106
157
  if (fired === 0) return "has not fired yet";
107
- const back = rule.trigger.side === "reply" ? `, sent back ${sentBack}` : "";
158
+ const back = rule.trigger?.side === "reply" ? `, sent back ${sentBack}` : "";
108
159
  return `fired ${fired}${back} · corrected after ${correctedAfter} of ${judged} judged`;
109
160
  }
110
161
 
111
- function Active({ rule }: { rule: ActiveRule }) {
162
+ function windowsLine({ before, after }: Pick<OpinionEffect, "before" | "after">): string {
163
+ return `${before.per100.toFixed(1)}/100 before (${before.count} in ${before.turns}) → ${after.per100.toFixed(1)}/100 after (${after.count} in ${after.turns})`;
164
+ }
165
+
166
+ function effectRows(rule: ActiveRule): [string, string | undefined][] {
167
+ const opinion = rule.opinionEffect;
168
+ if (!opinion) return [["effect", firingLine(rule)]];
169
+ return [
170
+ ["kind", opinion.kind],
171
+ ["effect", `${windowsLine(opinion)} · ${opinion.verdict}`],
172
+ ["other kinds", windowsLine(opinion.control)],
173
+ ];
174
+ }
175
+
176
+ function Active({ rule, onDecided }: { rule: ActiveRule; onDecided: () => void }) {
177
+ const { error, busy, decide } = useDecision(rule.id, onDecided);
112
178
  return (
113
179
  <div className="flex flex-col gap-2 border-t border-divider py-3 first:border-t-0">
114
180
  <div className="font-heading text-[15px] font-semibold">{rule.when}</div>
115
- <Facts rows={[...ruleFacts(rule), ["effect", effectLine(rule)]]} />
181
+ <Facts rows={[...ruleFacts(rule), ...effectRows(rule)]} />
182
+ <Refusal error={error} />
183
+ <div>
184
+ <Button disabled={busy} onClick={() => decide("retired")}>
185
+ Retire
186
+ </Button>
187
+ </div>
116
188
  </div>
117
189
  );
118
190
  }
@@ -230,7 +302,7 @@ export function Relationship() {
230
302
  {active.length === 0 ? (
231
303
  <Empty>No rule approved yet.</Empty>
232
304
  ) : (
233
- active.map((rule) => <Active key={rule.id} rule={rule} />)
305
+ active.map((rule) => <Active key={rule.id} rule={rule} onDecided={reload} />)
234
306
  )}
235
307
  </Panel>
236
308
  <LastDays view={view.data} />