letagents 0.12.31 → 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.
- package/dist/mcp/server/register-tools.js +2 -0
- package/dist/mcp/server/runtime/identity/config.js +30 -1
- package/dist/mcp/server/supervised-tool-facade.js +2 -17
- package/dist/mcp/server/tools/messages/wait-tool.js +1 -1
- package/dist/mcp/server/tools/wake-rules.js +96 -0
- package/dist/mcp/server.js +12 -0
- package/dist/shared/agent-identity.js +4 -0
- package/package.json +1 -1
- package/shared/activation-routing.d.mts +1 -1
- package/shared/room-resource-invalidation.d.mts +3 -0
- package/shared/room-resource-invalidation.mjs +2 -0
- package/shared/scratch-workspace-repository.d.mts +13 -0
- package/shared/scratch-workspace-repository.mjs +213 -0
- package/shared/supervised-read-tools.d.mts +2 -0
- package/shared/supervised-read-tools.mjs +25 -0
- package/shared/ui/WakeGlyph.vue +55 -0
- package/shared/ui/WakeRuleLine.vue +51 -0
- package/shared/ui/WakeRulesPanel.vue +201 -0
- package/shared/ui/wake-rules.css +273 -0
- package/shared/wake-rules.d.mts +145 -0
- package/shared/wake-rules.mjs +425 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span v-if="summary" class="wake-rule-line" :title="details">
|
|
3
|
+
<WakeGlyph />
|
|
4
|
+
<span class="wake-rule-line-label">{{ summary.label }}</span>
|
|
5
|
+
<span v-if="summary.detail" class="wake-rule-line-detail">· {{ summary.detail }}</span>
|
|
6
|
+
</span>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
|
11
|
+
import { formatWakeClock, summarizeWakeRuleParts, wakeRuleLabel, type WakeRule } from "../wake-rules.mjs";
|
|
12
|
+
import WakeGlyph from "./WakeGlyph.vue";
|
|
13
|
+
|
|
14
|
+
const props = defineProps<{ rules: readonly WakeRule[] }>();
|
|
15
|
+
const now = ref(new Date());
|
|
16
|
+
const summary = computed(() => summarizeWakeRuleParts(props.rules, now.value));
|
|
17
|
+
const details = computed(() => props.rules
|
|
18
|
+
.map((rule) => wakeRuleLabel(rule, { formatTime: (iso) => formatWakeClock(iso, now.value) }))
|
|
19
|
+
.join("\n"));
|
|
20
|
+
|
|
21
|
+
let tick: ReturnType<typeof setInterval> | undefined;
|
|
22
|
+
onMounted(() => { tick = setInterval(() => { now.value = new Date(); }, 30_000); });
|
|
23
|
+
onBeforeUnmount(() => clearInterval(tick));
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<style>
|
|
27
|
+
.wake-rule-line {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 5px;
|
|
31
|
+
min-width: 0;
|
|
32
|
+
max-width: 100%;
|
|
33
|
+
font-variant-numeric: tabular-nums slashed-zero;
|
|
34
|
+
}
|
|
35
|
+
.wake-rule-line > .wake-glyph {
|
|
36
|
+
width: 13px;
|
|
37
|
+
height: 13px;
|
|
38
|
+
opacity: 0.85;
|
|
39
|
+
}
|
|
40
|
+
.wake-rule-line-label {
|
|
41
|
+
min-width: 0;
|
|
42
|
+
overflow: hidden;
|
|
43
|
+
text-overflow: ellipsis;
|
|
44
|
+
white-space: nowrap;
|
|
45
|
+
}
|
|
46
|
+
/* How long, and how many more, stays visible when the label is shortened. */
|
|
47
|
+
.wake-rule-line-detail {
|
|
48
|
+
flex-shrink: 0;
|
|
49
|
+
white-space: nowrap;
|
|
50
|
+
}
|
|
51
|
+
</style>
|
|
@@ -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>
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
export type WakeRuleEvent =
|
|
2
|
+
| "timer"
|
|
3
|
+
| "task.status_changed"
|
|
4
|
+
| "github.check_completed"
|
|
5
|
+
| "github.review_submitted"
|
|
6
|
+
| "github.pr_closed";
|
|
7
|
+
export type WakeRuleStatus = "active" | "fired" | "expired" | "cancelled";
|
|
8
|
+
export type WakeTaskStatus =
|
|
9
|
+
| "proposed" | "accepted" | "assigned" | "in_progress" | "blocked" | "in_review" | "merged" | "done" | "cancelled";
|
|
10
|
+
export type WakeCheckConclusion =
|
|
11
|
+
| "success" | "failure" | "neutral" | "cancelled" | "skipped" | "timed_out" | "action_required" | "stale";
|
|
12
|
+
export type WakeReviewState = "approved" | "changes_requested" | "commented";
|
|
13
|
+
|
|
14
|
+
export interface WakeRuleArguments {
|
|
15
|
+
at?: string;
|
|
16
|
+
task_id?: string;
|
|
17
|
+
to?: WakeTaskStatus[];
|
|
18
|
+
branch?: string;
|
|
19
|
+
pr?: number;
|
|
20
|
+
mine?: true;
|
|
21
|
+
conclusions?: WakeCheckConclusion[];
|
|
22
|
+
states?: WakeReviewState[];
|
|
23
|
+
merged_only?: true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface WakeRuleInput {
|
|
27
|
+
event: WakeRuleEvent;
|
|
28
|
+
arguments?: Record<string, unknown>;
|
|
29
|
+
repeat?: boolean;
|
|
30
|
+
expires_at?: string;
|
|
31
|
+
note?: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ParsedWakeRuleInput {
|
|
35
|
+
event: WakeRuleEvent;
|
|
36
|
+
arguments: WakeRuleArguments;
|
|
37
|
+
repeat: boolean;
|
|
38
|
+
expires_at: string;
|
|
39
|
+
note: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WakeRuleActor { kind: "agent" | "human"; id: string; label: string }
|
|
43
|
+
|
|
44
|
+
/** A rule as the API returns it to agents, people and apps. */
|
|
45
|
+
export interface WakeRule extends ParsedWakeRuleInput {
|
|
46
|
+
id: string;
|
|
47
|
+
room_id: string;
|
|
48
|
+
agent_key: string;
|
|
49
|
+
agent_name: string;
|
|
50
|
+
status: WakeRuleStatus;
|
|
51
|
+
created_at: string;
|
|
52
|
+
updated_at: string;
|
|
53
|
+
fire_count: number;
|
|
54
|
+
last_fired_at: string | null;
|
|
55
|
+
/** The room message that last woke the agent, when it fired or expired. */
|
|
56
|
+
wake_message_id: string | null;
|
|
57
|
+
cancelled_by: WakeRuleActor | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface WakeRulePage {
|
|
61
|
+
room_id: string;
|
|
62
|
+
active: WakeRule[];
|
|
63
|
+
/** Rules that fired, expired or were cancelled in the last 7 days, newest first. */
|
|
64
|
+
recent: WakeRule[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Supplied by each app to the shared wake-rule components. */
|
|
68
|
+
export interface WakeRuleApi {
|
|
69
|
+
list(roomId: string): Promise<WakeRulePage>;
|
|
70
|
+
cancel(roomId: string, ruleId: string): Promise<WakeRule>;
|
|
71
|
+
restore(roomId: string, ruleId: string): Promise<WakeRule>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type WakeNoticeOutcome = "fired" | "expired";
|
|
75
|
+
|
|
76
|
+
export interface WakeCheckFact { name: string; conclusion: string; url: string | null }
|
|
77
|
+
export interface WakeCheckPushFact { head_ref: string | null; checks: WakeCheckFact[] }
|
|
78
|
+
export interface WakeOccurrenceFacts {
|
|
79
|
+
task_id?: string;
|
|
80
|
+
status?: string;
|
|
81
|
+
previous_status?: string;
|
|
82
|
+
pr?: number | null;
|
|
83
|
+
/** CI: every settled push that mattered, each with the latest result of its checks. */
|
|
84
|
+
pushes?: WakeCheckPushFact[];
|
|
85
|
+
reviewer?: string | null;
|
|
86
|
+
state?: string;
|
|
87
|
+
merged?: boolean;
|
|
88
|
+
url?: string | null;
|
|
89
|
+
/** The newest event considered, so a repeating rule only looks after it. */
|
|
90
|
+
cursor_at?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const WAKE_RULE_EVENTS: readonly WakeRuleEvent[];
|
|
94
|
+
export const WAKE_RULE_STATUSES: readonly WakeRuleStatus[];
|
|
95
|
+
export const WAKE_NOTICE_SOURCE: "wake_rule";
|
|
96
|
+
export const WAKE_RULE_LIMITS: Readonly<{
|
|
97
|
+
activePerAgent: number;
|
|
98
|
+
defaultTtlMs: number;
|
|
99
|
+
maxTtlMs: number;
|
|
100
|
+
minLeadMs: number;
|
|
101
|
+
noteMaxLength: number;
|
|
102
|
+
branchMaxLength: number;
|
|
103
|
+
repeatDebounceMs: number;
|
|
104
|
+
checkSettleMs: number;
|
|
105
|
+
timerGraceMs: number;
|
|
106
|
+
}>;
|
|
107
|
+
export const WAKE_TASK_STATUSES: readonly WakeTaskStatus[];
|
|
108
|
+
export const WAKE_CHECK_CONCLUSIONS: readonly WakeCheckConclusion[];
|
|
109
|
+
export const WAKE_REVIEW_STATES: readonly WakeReviewState[];
|
|
110
|
+
|
|
111
|
+
export class WakeRuleError extends Error { status: number; constructor(message: string, status?: number) }
|
|
112
|
+
|
|
113
|
+
export function parseWakeRuleInput(value: unknown, now?: Date): ParsedWakeRuleInput;
|
|
114
|
+
export function wakeRuleIdentityKey(event: WakeRuleEvent, args: WakeRuleArguments): string;
|
|
115
|
+
export function isFailedCheckConclusion(conclusion: string): boolean;
|
|
116
|
+
|
|
117
|
+
type Describable = Pick<WakeRule, "event" | "arguments">;
|
|
118
|
+
export interface WakeRuleDescribeOptions { formatTime?: (iso: string) => string }
|
|
119
|
+
export function describeWakeRule(rule: Describable, options?: WakeRuleDescribeOptions): { preposition: "for" | "until"; object: string };
|
|
120
|
+
export function wakeRuleLabel(rule: Describable, options?: WakeRuleDescribeOptions): string;
|
|
121
|
+
export function describeWakeOccurrence(rule: Describable, facts: WakeOccurrenceFacts): string;
|
|
122
|
+
export function formatWakeNotice(input: {
|
|
123
|
+
rule: Pick<WakeRule, "id" | "event" | "arguments" | "note" | "repeat" | "expires_at"> & { fire_count?: number };
|
|
124
|
+
outcome: WakeNoticeOutcome;
|
|
125
|
+
facts: WakeOccurrenceFacts;
|
|
126
|
+
agentName: string | null | undefined;
|
|
127
|
+
}): { text: string; display_text: string };
|
|
128
|
+
|
|
129
|
+
export function formatWakeClock(iso: string, now?: Date, locale?: string): string;
|
|
130
|
+
export function formatWakeSpan(ms: number): string;
|
|
131
|
+
export function wakeRuleTiming(
|
|
132
|
+
rule: Pick<WakeRule, "event" | "arguments" | "created_at">,
|
|
133
|
+
now?: Date,
|
|
134
|
+
locale?: string,
|
|
135
|
+
): { text: string; datetime: string };
|
|
136
|
+
export function summarizeWakeRuleParts(
|
|
137
|
+
rules: readonly Pick<WakeRule, "event" | "arguments" | "created_at">[] | null | undefined,
|
|
138
|
+
now?: Date,
|
|
139
|
+
locale?: string,
|
|
140
|
+
): { label: string; detail: string } | null;
|
|
141
|
+
export function summarizeWakeRules(
|
|
142
|
+
rules: readonly Pick<WakeRule, "event" | "arguments" | "created_at">[] | null | undefined,
|
|
143
|
+
now?: Date,
|
|
144
|
+
locale?: string,
|
|
145
|
+
): string | null;
|