letagents 0.12.30 → 0.12.32

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 (53) hide show
  1. package/README.md +1 -1
  2. package/dist/mcp/local-state/storage.js +0 -5
  3. package/dist/mcp/server/register-tools.js +2 -0
  4. package/dist/mcp/server/runtime/api.js +53 -1
  5. package/dist/mcp/server/runtime/identity/config.js +30 -1
  6. package/dist/mcp/server/runtime/identity/liveness.js +19 -1
  7. package/dist/mcp/server/runtime/process-connection.js +239 -0
  8. package/dist/mcp/server/runtime/worker-bearer.js +0 -4
  9. package/dist/mcp/server/runtime/worker-handles.js +109 -15
  10. package/dist/mcp/server/runtime.js +1 -1
  11. package/dist/mcp/server/supervised-tool-facade.js +2 -17
  12. package/dist/mcp/server/tools/agent-sessions.js +38 -6
  13. package/dist/mcp/server/tools/knowledge.js +29 -0
  14. package/dist/mcp/server/tools/messages/wait-tool.js +1 -1
  15. package/dist/mcp/server/tools/rooms/inspection-tools.js +1 -1
  16. package/dist/mcp/server/tools/wake-rules.js +96 -0
  17. package/dist/mcp/server.js +29 -7
  18. package/dist/mcp/worker-call-context.js +4 -0
  19. package/dist/shared/agent-identity.js +4 -0
  20. package/dist/shared/agent-presence.js +141 -0
  21. package/dist/shared/codenames.js +2 -104
  22. package/dist/shared/handoff.js +0 -6
  23. package/dist/shared/request-headers.js +11 -0
  24. package/dist/shared/room-agent-prompts.js +1 -1
  25. package/package.json +4 -5
  26. package/shared/activation-routing.d.mts +6 -1
  27. package/shared/activation-routing.mjs +10 -1
  28. package/shared/agent-codenames.d.mts +17 -0
  29. package/shared/agent-codenames.mjs +96 -0
  30. package/shared/daemon-maintenance.d.mts +13 -0
  31. package/shared/daemon-maintenance.mjs +99 -0
  32. package/shared/permission-review.d.mts +30 -0
  33. package/shared/permission-review.mjs +482 -0
  34. package/shared/provider-acquisition-evidence.d.mts +15 -0
  35. package/shared/provider-acquisition-evidence.mjs +33 -0
  36. package/shared/room-agent-work.d.mts +1 -0
  37. package/shared/room-agent-work.mjs +11 -0
  38. package/shared/room-resource-invalidation.d.mts +3 -0
  39. package/shared/room-resource-invalidation.mjs +2 -0
  40. package/shared/room-settings.d.mts +48 -0
  41. package/shared/room-settings.mjs +108 -0
  42. package/shared/scratch-workspace-repository.d.mts +13 -0
  43. package/shared/scratch-workspace-repository.mjs +213 -0
  44. package/shared/supervised-read-tools.d.mts +2 -0
  45. package/shared/supervised-read-tools.mjs +25 -0
  46. package/shared/ui/PrivateMessages.vue +12 -17
  47. package/shared/ui/WakeGlyph.vue +55 -0
  48. package/shared/ui/WakeRuleLine.vue +51 -0
  49. package/shared/ui/WakeRulesPanel.vue +201 -0
  50. package/shared/ui/private-messages.css +35 -25
  51. package/shared/ui/wake-rules.css +273 -0
  52. package/shared/wake-rules.d.mts +145 -0
  53. package/shared/wake-rules.mjs +425 -0
@@ -0,0 +1,201 @@
1
+ <template>
2
+ <section v-if="rows.length || recent.length" ref="root" class="wake-rules" aria-label="Wake rules">
3
+ <TransitionGroup tag="ul" name="wake-rule" class="wake-rules-active">
4
+ <li
5
+ v-for="row in rows"
6
+ :key="row.rule.id"
7
+ class="wake-rule"
8
+ :data-rule-id="row.rule.id"
9
+ :data-state="row.cancelled ? 'cancelled' : 'active'"
10
+ >
11
+ <span class="wake-rule-mark"><WakeGlyph :state="row.cancelled ? 'ended' : 'waiting'" /></span>
12
+ <div class="wake-rule-copy">
13
+ <p class="wake-rule-kicker">{{ row.cancelled ? "Cancelled" : `Waiting ${describe(row.rule).preposition}` }}</p>
14
+ <strong>{{ describe(row.rule).object }}</strong>
15
+ <span v-if="row.rule.note" class="wake-rule-note">{{ row.rule.note }}</span>
16
+ <span class="wake-rule-meta">
17
+ <template v-if="row.rule.event === 'timer'">
18
+ Wakes in <time :datetime="row.rule.arguments.at" :title="fullTime(row.rule.arguments.at)">{{ until(row.rule.arguments.at!) }}</time>
19
+ </template>
20
+ <template v-else>
21
+ <time :datetime="row.rule.created_at" :title="`Since ${fullTime(row.rule.created_at)}`">{{ span(row.rule.created_at) }}</time> so far
22
+ · {{ row.rule.repeat ? "Repeats until" : "Expires" }}
23
+ <time :datetime="row.rule.expires_at" :title="fullTime(row.rule.expires_at)">{{ clock(row.rule.expires_at) }}</time>
24
+ </template>
25
+ </span>
26
+ <span v-if="row.error" class="wake-rule-error" role="status">{{ row.error }}</span>
27
+ </div>
28
+ <button
29
+ v-if="row.cancelled"
30
+ type="button"
31
+ class="wake-rule-action"
32
+ data-tone="undo"
33
+ :disabled="busy.has(row.rule.id)"
34
+ @click="undo(row.rule)"
35
+ >Undo</button>
36
+ <button
37
+ v-else-if="api"
38
+ type="button"
39
+ class="wake-rule-action"
40
+ :disabled="busy.has(row.rule.id)"
41
+ :aria-label="`Cancel wake rule: waiting ${describe(row.rule).preposition} ${describe(row.rule).object}`"
42
+ @click="cancel(row.rule)"
43
+ >Cancel</button>
44
+ </li>
45
+ </TransitionGroup>
46
+
47
+ <details v-if="recent.length" class="wake-rules-recent">
48
+ <summary>Recent wakes <span>{{ recent.length }}</span></summary>
49
+ <ul>
50
+ <li v-for="rule in recent" :key="rule.id" :data-status="rule.status">
51
+ <WakeGlyph :state="rule.status === 'fired' ? 'woke' : 'ended'" still />
52
+ <span class="wake-rules-recent-copy">
53
+ <strong>{{ outcomeLabel(rule) }}</strong>
54
+ <span>{{ describe(rule).preposition === "until" ? `until ${describe(rule).object}` : describe(rule).object }}</span>
55
+ </span>
56
+ <button
57
+ v-if="rule.wake_message_id"
58
+ type="button"
59
+ class="wake-rules-recent-time"
60
+ :title="`Show the wake message · ${fullTime(endedAt(rule))}`"
61
+ @click="emit('open-message', rule.wake_message_id)"
62
+ ><time :datetime="endedAt(rule)">{{ clock(endedAt(rule)) }}</time></button>
63
+ <time v-else class="wake-rules-recent-time" :datetime="endedAt(rule)" :title="fullTime(endedAt(rule))">{{ clock(endedAt(rule)) }}</time>
64
+ </li>
65
+ </ul>
66
+ </details>
67
+ </section>
68
+ </template>
69
+
70
+ <script setup lang="ts">
71
+ import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
72
+ import {
73
+ describeWakeRule,
74
+ formatWakeClock,
75
+ formatWakeSpan,
76
+ type WakeRule,
77
+ type WakeRuleApi,
78
+ } from "../wake-rules.mjs";
79
+ import WakeGlyph from "./WakeGlyph.vue";
80
+
81
+ const props = withDefaults(defineProps<{
82
+ roomId: string;
83
+ /** The agent's active rules, soonest first. */
84
+ rules: readonly WakeRule[];
85
+ recent?: readonly WakeRule[];
86
+ /** Without an API the panel is read-only. */
87
+ api?: Pick<WakeRuleApi, "cancel" | "restore"> | null;
88
+ }>(), { recent: () => [], api: null });
89
+ const emit = defineEmits<{ "open-message": [messageId: string] }>();
90
+
91
+ /** How long a cancelled rule stays in place with Undo before it leaves. */
92
+ const UNDO_WINDOW_MS = 8000;
93
+ const now = ref(new Date());
94
+ const busy = reactive(new Set<string>());
95
+ const errors = reactive(new Map<string, string>());
96
+ const cancelled = reactive(new Map<string, { rule: WakeRule; timer: ReturnType<typeof setTimeout> }>());
97
+ /**
98
+ * Restored rules stay on screen until the room's lists catch up: back among
99
+ * the active rules, or among recent ones if anything that happened while it
100
+ * was cancelled woke the agent straight away.
101
+ */
102
+ const restored = reactive(new Map<string, WakeRule>());
103
+ watch(() => [props.rules, props.recent] as const, ([rules, recent]) => {
104
+ for (const rule of [...rules, ...recent]) {
105
+ if (rule.status !== "cancelled") restored.delete(rule.id);
106
+ }
107
+ });
108
+
109
+ // Undo happens where the action was: a cancelled rule keeps its row until
110
+ // the window closes, even after the room's list no longer includes it.
111
+ const rows = computed(() => {
112
+ const active = props.rules.filter((rule) => !cancelled.has(rule.id));
113
+ const held = [...cancelled.values()].map(({ rule }) => rule);
114
+ const revived = [...restored.values()].filter((rule) => !cancelled.has(rule.id) && !props.rules.some((candidate) => candidate.id === rule.id));
115
+ return [...active, ...held, ...revived]
116
+ .sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id))
117
+ .map((rule) => ({ rule, cancelled: cancelled.has(rule.id), error: errors.get(rule.id) ?? null }));
118
+ });
119
+
120
+ const describe = (rule: Pick<WakeRule, "event" | "arguments">) =>
121
+ describeWakeRule(rule, { formatTime: (iso) => formatWakeClock(iso, now.value) });
122
+ const clock = (iso: string) => formatWakeClock(iso, now.value);
123
+ const span = (iso: string) => formatWakeSpan(now.value.getTime() - Date.parse(iso));
124
+ const until = (iso: string) => formatWakeSpan(Date.parse(iso) - now.value.getTime());
125
+ const fullTime = (iso: string | undefined) => iso
126
+ ? new Date(iso).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" })
127
+ : "";
128
+ const endedAt = (rule: WakeRule) => rule.last_fired_at ?? rule.updated_at;
129
+ const root = ref<HTMLElement | null>(null);
130
+
131
+ /**
132
+ * Cancel and Undo replace each other in the same place. When the pressed
133
+ * button was focused, focus moves to its replacement instead of the page.
134
+ */
135
+ async function keepFocusOnRow(ruleId: string, hadFocus: boolean): Promise<void> {
136
+ if (!hadFocus) return;
137
+ await nextTick();
138
+ root.value?.querySelector<HTMLButtonElement>(`[data-rule-id="${CSS.escape(ruleId)}"] .wake-rule-action`)?.focus();
139
+ }
140
+
141
+ function rowHasFocus(ruleId: string): boolean {
142
+ const active = document.activeElement;
143
+ return Boolean(active && root.value?.querySelector(`[data-rule-id="${CSS.escape(ruleId)}"]`)?.contains(active));
144
+ }
145
+
146
+ function outcomeLabel(rule: WakeRule): string {
147
+ if (rule.status === "fired" || (rule.status === "active" && rule.fire_count > 0)) return "Woke";
148
+ if (rule.status === "expired") return "Stopped waiting";
149
+ return rule.cancelled_by ? `Cancelled by ${rule.cancelled_by.label}` : "Cancelled";
150
+ }
151
+
152
+ function release(ruleId: string): void {
153
+ const held = cancelled.get(ruleId);
154
+ if (held) clearTimeout(held.timer);
155
+ cancelled.delete(ruleId);
156
+ }
157
+
158
+ async function cancel(rule: WakeRule): Promise<void> {
159
+ if (!props.api || busy.has(rule.id)) return;
160
+ const hadFocus = rowHasFocus(rule.id);
161
+ busy.add(rule.id);
162
+ errors.delete(rule.id);
163
+ cancelled.set(rule.id, { rule, timer: setTimeout(() => release(rule.id), UNDO_WINDOW_MS) });
164
+ try {
165
+ await props.api.cancel(props.roomId, rule.id);
166
+ } catch (error) {
167
+ release(rule.id);
168
+ errors.set(rule.id, error instanceof Error ? error.message : "Couldn't cancel this wake rule. Try again.");
169
+ } finally {
170
+ busy.delete(rule.id);
171
+ }
172
+ // Undo (or Cancel again, if cancelling failed) is enabled only now.
173
+ await keepFocusOnRow(rule.id, hadFocus);
174
+ }
175
+
176
+ async function undo(rule: WakeRule): Promise<void> {
177
+ if (!props.api || busy.has(rule.id)) return;
178
+ const hadFocus = rowHasFocus(rule.id);
179
+ busy.add(rule.id);
180
+ try {
181
+ restored.set(rule.id, await props.api.restore(props.roomId, rule.id));
182
+ release(rule.id);
183
+ } catch (error) {
184
+ errors.set(rule.id, error instanceof Error ? error.message : "Couldn't restore this wake rule.");
185
+ } finally {
186
+ busy.delete(rule.id);
187
+ }
188
+ await keepFocusOnRow(rule.id, hadFocus);
189
+ }
190
+
191
+ let tick: ReturnType<typeof setInterval> | undefined;
192
+ onMounted(() => {
193
+ tick = setInterval(() => { now.value = new Date(); }, 30_000);
194
+ });
195
+ onBeforeUnmount(() => {
196
+ clearInterval(tick);
197
+ for (const ruleId of [...cancelled.keys()]) release(ruleId);
198
+ });
199
+ </script>
200
+
201
+ <style src="./wake-rules.css"></style>
@@ -1,8 +1,10 @@
1
1
  .private-messages {
2
2
  display: grid;
3
- grid-template-columns: 280px minmax(0, 1fr);
3
+ grid-template-columns: 300px minmax(0, 1fr);
4
+ grid-template-rows: minmax(0, 1fr);
4
5
  flex: 1;
5
6
  min-height: 0;
7
+ min-width: 0;
6
8
  height: 100%;
7
9
  background: var(--bg);
8
10
  color: var(--text);
@@ -66,13 +68,24 @@
66
68
  min-height: 0;
67
69
  border-right: 1px solid var(--border);
68
70
  background: var(--bg-subtle);
69
- padding: 18px 10px 10px;
71
+ padding: 16px 12px 12px;
70
72
  }
71
73
  .conversation-list-heading {
72
74
  display: flex;
73
75
  align-items: center;
74
76
  justify-content: space-between;
75
- padding: 0 8px 18px;
77
+ gap: 12px;
78
+ min-height: 36px;
79
+ padding: 0 4px 16px;
80
+ }
81
+ .private-messages .conversation-new-chat {
82
+ min-height: 32px;
83
+ padding: 6px 10px;
84
+ font-size: 12px;
85
+ }
86
+ .conversation-new-chat svg {
87
+ width: 15px;
88
+ height: 15px;
76
89
  }
77
90
  .chat-icon-button {
78
91
  display: inline-flex;
@@ -148,6 +161,7 @@
148
161
  }
149
162
  .conversation-rows {
150
163
  overflow: auto;
164
+ overscroll-behavior: contain;
151
165
  min-height: 0;
152
166
  flex: 1;
153
167
  }
@@ -238,13 +252,8 @@
238
252
  padding: 32px 18px;
239
253
  font-size: 12px;
240
254
  }
241
- .conversation-list-empty > svg {
242
- width: 26px;
243
- height: 26px;
244
- margin-bottom: 10px;
245
- }
246
- .conversation-list-empty p {
247
- margin-bottom: 8px;
255
+ .conversation-list-empty-initial {
256
+ display: none;
248
257
  }
249
258
  .chat-text-button {
250
259
  border: 0;
@@ -547,9 +556,9 @@
547
556
  max-width: 430px;
548
557
  }
549
558
  .conversation-welcome h2 {
550
- font-size: 22px;
551
- font-weight: 550;
552
- letter-spacing: -0.03em;
559
+ font-size: 18px;
560
+ font-weight: 600;
561
+ letter-spacing: -0.02em;
553
562
  }
554
563
  .conversation-welcome p,
555
564
  .conversation-timeline-empty p {
@@ -557,20 +566,13 @@
557
566
  font-size: 13px;
558
567
  line-height: 1.7;
559
568
  }
560
- .conversation-welcome .chat-primary {
561
- margin-top: 8px;
562
- }
563
569
  .conversation-empty-mark {
564
- width: 52px;
565
- height: 52px;
570
+ width: 40px;
571
+ height: 40px;
566
572
  display: flex;
567
573
  align-items: center;
568
574
  justify-content: center;
569
- background: var(--bg-card);
570
- border: 1px solid var(--border-strong);
571
- border-radius: 16px;
572
575
  margin-bottom: 6px;
573
- box-shadow: inset 0 1px 0 var(--border);
574
576
  }
575
577
  .conversation-empty-mark svg {
576
578
  width: 24px;
@@ -583,6 +585,8 @@
583
585
  letter-spacing: -0.02em;
584
586
  }
585
587
  .conversation-compose-people {
588
+ min-height: 0;
589
+ overflow-y: auto;
586
590
  padding: 28px;
587
591
  max-width: 740px;
588
592
  width: 100%;
@@ -742,9 +746,9 @@
742
746
  color: var(--text);
743
747
  }
744
748
  }
745
- @media (max-width: 1000px) {
749
+ @container private-messages (max-width: 900px) {
746
750
  .private-messages {
747
- grid-template-columns: 230px minmax(0, 1fr);
751
+ grid-template-columns: 260px minmax(0, 1fr);
748
752
  }
749
753
  .conversation-header {
750
754
  padding: 14px 18px;
@@ -756,13 +760,19 @@
756
760
  padding: 12px 20px 16px;
757
761
  }
758
762
  }
759
- @media (max-width: 720px) {
763
+ @container private-messages (max-width: 640px) {
760
764
  .private-messages {
761
765
  grid-template-columns: minmax(0, 1fr);
762
766
  }
763
767
  .conversation-list {
764
768
  border: 0;
765
769
  }
770
+ .conversation-list-empty-initial {
771
+ display: block;
772
+ }
773
+ .private-messages .conversation-new-chat {
774
+ min-height: 40px;
775
+ }
766
776
  .conversation-content {
767
777
  display: none;
768
778
  }
@@ -0,0 +1,273 @@
1
+ /*
2
+ * Wake rules read as a quiet state, not an alert: neutral colour, one glyph,
3
+ * and motion only when a person changes something.
4
+ */
5
+ .wake-rules {
6
+ display: grid;
7
+ gap: 8px;
8
+ }
9
+ .wake-rules ul {
10
+ margin: 0;
11
+ padding: 0;
12
+ list-style: none;
13
+ }
14
+ .wake-rules time,
15
+ .wake-rule-copy strong {
16
+ font-variant-numeric: tabular-nums slashed-zero;
17
+ }
18
+
19
+ .wake-rules-active {
20
+ position: relative;
21
+ display: grid;
22
+ gap: 8px;
23
+ }
24
+ .wake-rule {
25
+ display: grid;
26
+ grid-template-columns: 30px minmax(0, 1fr) auto;
27
+ gap: 11px;
28
+ align-items: start;
29
+ padding: 14px;
30
+ border: 1px solid color-mix(in srgb, var(--text) 7.5%, transparent);
31
+ border-radius: 16px;
32
+ background: color-mix(in srgb, var(--text) 2.6%, transparent);
33
+ }
34
+ .wake-rule-mark {
35
+ display: grid;
36
+ place-items: center;
37
+ width: 30px;
38
+ height: 30px;
39
+ border-radius: 10px;
40
+ background: color-mix(in srgb, var(--text-secondary) 12%, transparent);
41
+ color: var(--text-secondary);
42
+ }
43
+ .wake-rule-copy {
44
+ min-width: 0;
45
+ }
46
+ .wake-rule-kicker {
47
+ margin: 0;
48
+ color: var(--text-secondary);
49
+ font-size: 0.64rem;
50
+ font-weight: 760;
51
+ letter-spacing: 0.08em;
52
+ text-transform: uppercase;
53
+ }
54
+ .wake-rule-copy strong {
55
+ display: block;
56
+ margin-top: 4px;
57
+ color: var(--text);
58
+ font-size: 0.84rem;
59
+ font-weight: 640;
60
+ line-height: 1.45;
61
+ overflow-wrap: anywhere;
62
+ }
63
+ .wake-rule-note {
64
+ display: block;
65
+ margin-top: 3px;
66
+ color: var(--text-secondary);
67
+ font-size: 0.75rem;
68
+ line-height: 1.45;
69
+ overflow-wrap: anywhere;
70
+ }
71
+ .wake-rule-meta {
72
+ display: block;
73
+ margin-top: 6px;
74
+ color: var(--text-tertiary);
75
+ font-size: 0.69rem;
76
+ line-height: 1.4;
77
+ }
78
+ .wake-rule-error {
79
+ display: block;
80
+ margin-top: 6px;
81
+ color: var(--red-text);
82
+ font-size: 0.71rem;
83
+ line-height: 1.4;
84
+ }
85
+ .wake-rule[data-state="cancelled"] {
86
+ border-style: dashed;
87
+ background: transparent;
88
+ }
89
+ .wake-rule[data-state="cancelled"] .wake-rule-copy strong {
90
+ color: var(--text-secondary);
91
+ text-decoration: line-through;
92
+ text-decoration-color: color-mix(in srgb, var(--text-secondary) 45%, transparent);
93
+ }
94
+
95
+ .wake-rule-action {
96
+ align-self: center;
97
+ min-height: 32px;
98
+ padding: 0 11px;
99
+ border: 1px solid color-mix(in srgb, var(--text) 12%, transparent);
100
+ border-radius: 9px;
101
+ background: transparent;
102
+ color: var(--text-secondary);
103
+ font: inherit;
104
+ font-size: 0.72rem;
105
+ font-weight: 650;
106
+ cursor: pointer;
107
+ transition:
108
+ transform 120ms var(--ease-out),
109
+ background-color 140ms ease,
110
+ border-color 140ms ease,
111
+ color 140ms ease;
112
+ }
113
+ .wake-rule-action[data-tone="undo"] {
114
+ border-color: color-mix(in srgb, var(--blue) 38%, transparent);
115
+ color: var(--blue);
116
+ }
117
+ @media (hover: hover) and (pointer: fine) {
118
+ .wake-rule-action:hover:not(:disabled) {
119
+ background: color-mix(in srgb, var(--text) 6%, transparent);
120
+ color: var(--text);
121
+ }
122
+ .wake-rule-action[data-tone="undo"]:hover:not(:disabled) {
123
+ background: color-mix(in srgb, var(--blue) 10%, transparent);
124
+ color: var(--blue);
125
+ }
126
+ }
127
+ .wake-rule-action:active:not(:disabled) {
128
+ transform: scale(0.97);
129
+ }
130
+ .wake-rule-action:focus-visible,
131
+ .wake-rules-recent summary:focus-visible,
132
+ .wake-rules-recent-time:focus-visible {
133
+ outline: 2px solid var(--blue);
134
+ outline-offset: 2px;
135
+ }
136
+ .wake-rule-action:disabled {
137
+ cursor: default;
138
+ opacity: 0.5;
139
+ }
140
+
141
+ /* A person's cancel or undo is the only change that moves a row. */
142
+ .wake-rule-enter-active {
143
+ transition: opacity 220ms var(--ease-out), transform 220ms var(--ease-out);
144
+ }
145
+ .wake-rule-enter-from {
146
+ opacity: 0;
147
+ transform: translateY(4px) scale(0.98);
148
+ }
149
+ .wake-rule-leave-active {
150
+ position: absolute;
151
+ inset-inline: 0;
152
+ pointer-events: none;
153
+ transition: opacity 150ms ease-in, transform 150ms ease-in;
154
+ }
155
+ .wake-rule-leave-to {
156
+ opacity: 0;
157
+ transform: scale(0.99);
158
+ }
159
+ .wake-rule-move {
160
+ transition: transform 220ms var(--ease-out);
161
+ }
162
+
163
+ .wake-rules-recent summary {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 6px;
167
+ width: fit-content;
168
+ padding: 4px 2px;
169
+ border-radius: 6px;
170
+ color: var(--text-secondary);
171
+ font-size: 0.64rem;
172
+ font-weight: 760;
173
+ letter-spacing: 0.08em;
174
+ text-transform: uppercase;
175
+ list-style: none;
176
+ cursor: pointer;
177
+ }
178
+ .wake-rules-recent summary::-webkit-details-marker {
179
+ display: none;
180
+ }
181
+ .wake-rules-recent summary::before {
182
+ content: "";
183
+ width: 5px;
184
+ height: 5px;
185
+ border-right: 1.5px solid currentColor;
186
+ border-bottom: 1.5px solid currentColor;
187
+ rotate: -45deg;
188
+ transition: rotate 160ms var(--ease-out);
189
+ }
190
+ .wake-rules-recent[open] summary::before {
191
+ rotate: 45deg;
192
+ }
193
+ .wake-rules-recent summary span {
194
+ color: var(--text-tertiary);
195
+ letter-spacing: 0;
196
+ }
197
+ .wake-rules-recent li {
198
+ display: grid;
199
+ grid-template-columns: 16px minmax(0, 1fr) auto;
200
+ gap: 9px;
201
+ align-items: center;
202
+ padding: 7px 2px;
203
+ color: var(--text-tertiary);
204
+ font-size: 0.73rem;
205
+ line-height: 1.4;
206
+ }
207
+ .wake-rules-recent li[data-status="fired"] > .wake-glyph {
208
+ color: var(--text-secondary);
209
+ }
210
+ .wake-rules-recent-copy {
211
+ display: flex;
212
+ gap: 6px;
213
+ min-width: 0;
214
+ }
215
+ .wake-rules-recent-copy strong {
216
+ flex-shrink: 0;
217
+ color: var(--text-secondary);
218
+ font-weight: 650;
219
+ }
220
+ .wake-rules-recent-copy span {
221
+ overflow: hidden;
222
+ text-overflow: ellipsis;
223
+ white-space: nowrap;
224
+ }
225
+ .wake-rules-recent-time {
226
+ min-height: 0;
227
+ padding: 2px 4px;
228
+ border: 0;
229
+ border-radius: 5px;
230
+ background: transparent;
231
+ color: var(--text-tertiary);
232
+ font: inherit;
233
+ font-size: 0.69rem;
234
+ line-height: 1.4;
235
+ }
236
+ /* An inline link: compact row, generous target. Hosts may size buttons for touch. */
237
+ .wake-rules button.wake-rules-recent-time {
238
+ position: relative;
239
+ min-height: 0;
240
+ cursor: pointer;
241
+ }
242
+ .wake-rules button.wake-rules-recent-time::after {
243
+ content: "";
244
+ position: absolute;
245
+ inset: -8px -6px;
246
+ }
247
+ @media (hover: hover) and (pointer: fine) {
248
+ button.wake-rules-recent-time:hover {
249
+ color: var(--text);
250
+ text-decoration: underline;
251
+ text-underline-offset: 2px;
252
+ }
253
+ }
254
+
255
+ @media (prefers-reduced-motion: reduce) {
256
+ .wake-rule-enter-active,
257
+ .wake-rule-leave-active,
258
+ .wake-rule-move {
259
+ transition: opacity 150ms ease;
260
+ }
261
+ .wake-rule-enter-from,
262
+ .wake-rule-leave-to {
263
+ transform: none;
264
+ }
265
+ .wake-rules-recent summary::before {
266
+ transition: none;
267
+ }
268
+ }
269
+ @media (prefers-contrast: more) {
270
+ .wake-rule {
271
+ border-color: color-mix(in srgb, var(--text) 28%, transparent);
272
+ }
273
+ }