@sigrun/escalations 0.2.0 → 0.2.1
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/CHANGELOG.md +28 -12
- package/dist/inbox.d.ts +9 -4
- package/dist/inbox.js +100 -59
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.1] - 2026-11-06
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The Inbox is one split page: the tab's list in the sidebar (tabs stay
|
|
15
|
+
in the header), the selected escalation in main, with its actions on
|
|
16
|
+
a header callout and a `Viewing` badge on the row being read. Params
|
|
17
|
+
are `{tab?, id}`: an id with no tab lists the item's own tab (notice
|
|
18
|
+
and tool links), opening a row keeps the current tab, and compact
|
|
19
|
+
widths use the page selection drill-in (list first, back to the tab).
|
|
20
|
+
An unknown or invisible id keeps the caller's list and shows "not
|
|
21
|
+
found" in main (identical for both — no title leak).
|
|
22
|
+
|
|
23
|
+
## [0.2.0] - 2026-11-05
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Published hooks `escalation.raise`, `escalation.take` and
|
|
28
|
+
`escalation.resolve`: the extension-to-extension seam over the same
|
|
29
|
+
store and lifecycle as the `escalate` tool. `take` claims an ANSWERED
|
|
30
|
+
item for the calling conversation (idempotent per conversation,
|
|
31
|
+
caller's bucket only); `resolve` needs the claiming conversation
|
|
32
|
+
(wrong_claimant propagates). The warden is the first consumer
|
|
33
|
+
(one-shot job grants).
|
|
34
|
+
|
|
8
35
|
## [0.1.1] - 2026-11-03
|
|
9
36
|
### Fixed
|
|
10
37
|
- `reopenReason` is cleared on forward transitions (`resolve` done,
|
|
@@ -28,18 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
28
55
|
- The factory accepts an optional `{ now }` clock for tests (arity one
|
|
29
56
|
is preserved for the loader).
|
|
30
57
|
|
|
31
|
-
## [
|
|
32
|
-
## [0.2.0] - 2026-11-05
|
|
33
|
-
|
|
34
|
-
### Added
|
|
35
|
-
|
|
36
|
-
- Published hooks `escalation.raise`, `escalation.take` and
|
|
37
|
-
`escalation.resolve`: the extension-to-extension seam over the same
|
|
38
|
-
store and lifecycle as the `escalate` tool. `take` claims an ANSWERED
|
|
39
|
-
item for the calling conversation (idempotent per conversation,
|
|
40
|
-
caller's bucket only); `resolve` needs the claiming conversation
|
|
41
|
-
(wrong_claimant propagates). The warden is the first consumer
|
|
42
|
-
(one-shot job grants).
|
|
58
|
+
## [0.1.0] - 2026-11-02
|
|
43
59
|
|
|
44
60
|
### Added
|
|
45
61
|
- The escalation extension factory with `escalate`, `escalation_list`, and
|
package/dist/inbox.d.ts
CHANGED
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
* The open tab sorts reopened items first, then newest; resolved is
|
|
9
9
|
* newest first. An answered `next-run` item whose job is disabled
|
|
10
10
|
* shows the badge `Waiting: job disabled`.
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
11
|
+
* - Split layout: the list is the page sidebar (tabs in the header),
|
|
12
|
+
* the selected item (params `{tab?, id}`) fills main, and the row
|
|
13
|
+
* being viewed carries a `Viewing` badge. An id with no tab lists the
|
|
14
|
+
* item's own tab; on compact widths the page's selection drill-in
|
|
15
|
+
* shows the list first.
|
|
16
|
+
* - The detail (main) is a header callout with the item's actions, the
|
|
17
|
+
* markdown body, answer form (options in the agent's order, NONE marked
|
|
13
18
|
* recommended — the webui preselects recommended options and the
|
|
14
19
|
* proposed action may be destructive), snooze form seeded at 7 days
|
|
15
|
-
* (number fields have no `default` prop), Dismiss/Withdraw/Restore
|
|
16
|
-
*
|
|
20
|
+
* (number fields have no `default` prop), Dismiss/Withdraw/Restore and
|
|
21
|
+
* conversation links on the header callout, and the history as a log.
|
|
17
22
|
* Orphaned items show only the pinned warn callout and Dismiss.
|
|
18
23
|
* - Every action resolves the item through `findVisible` over the
|
|
19
24
|
* caller's buckets, so another user's id behaves exactly like an
|
package/dist/inbox.js
CHANGED
|
@@ -8,12 +8,17 @@
|
|
|
8
8
|
* The open tab sorts reopened items first, then newest; resolved is
|
|
9
9
|
* newest first. An answered `next-run` item whose job is disabled
|
|
10
10
|
* shows the badge `Waiting: job disabled`.
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
11
|
+
* - Split layout: the list is the page sidebar (tabs in the header),
|
|
12
|
+
* the selected item (params `{tab?, id}`) fills main, and the row
|
|
13
|
+
* being viewed carries a `Viewing` badge. An id with no tab lists the
|
|
14
|
+
* item's own tab; on compact widths the page's selection drill-in
|
|
15
|
+
* shows the list first.
|
|
16
|
+
* - The detail (main) is a header callout with the item's actions, the
|
|
17
|
+
* markdown body, answer form (options in the agent's order, NONE marked
|
|
13
18
|
* recommended — the webui preselects recommended options and the
|
|
14
19
|
* proposed action may be destructive), snooze form seeded at 7 days
|
|
15
|
-
* (number fields have no `default` prop), Dismiss/Withdraw/Restore
|
|
16
|
-
*
|
|
20
|
+
* (number fields have no `default` prop), Dismiss/Withdraw/Restore and
|
|
21
|
+
* conversation links on the header callout, and the history as a log.
|
|
17
22
|
* Orphaned items show only the pinned warn callout and Dismiss.
|
|
18
23
|
* - Every action resolves the item through `findVisible` over the
|
|
19
24
|
* caller's buckets, so another user's id behaves exactly like an
|
|
@@ -32,6 +37,16 @@ const TABS = [
|
|
|
32
37
|
{ id: "resolved", label: "Resolved" },
|
|
33
38
|
];
|
|
34
39
|
const ACTIVE_STATUSES = ["answered", "claimed", "snoozed"];
|
|
40
|
+
/** The tab an item of this status lists under. */
|
|
41
|
+
function tabOf(status) {
|
|
42
|
+
if (status === "open")
|
|
43
|
+
return "open";
|
|
44
|
+
if (ACTIVE_STATUSES.includes(status))
|
|
45
|
+
return "active";
|
|
46
|
+
if (status === "dismissed")
|
|
47
|
+
return "dismissed";
|
|
48
|
+
return "resolved";
|
|
49
|
+
}
|
|
35
50
|
function isObject(payload) {
|
|
36
51
|
return typeof payload === "object" && payload !== null && !Array.isArray(payload);
|
|
37
52
|
}
|
|
@@ -182,6 +197,8 @@ export function createInboxView(deps) {
|
|
|
182
197
|
// Empty badge text is filtered by the channel, so the waiting
|
|
183
198
|
// badge shows only on rows that carry it.
|
|
184
199
|
{ text: { $: "./waitingBadge" }, tone: "warn" },
|
|
200
|
+
// Marks the row whose detail fills main (split layout).
|
|
201
|
+
{ text: { $: "./viewing" }, tone: "accent" },
|
|
185
202
|
],
|
|
186
203
|
meta: [{ time: { $: "./updatedAt" }, format: "relative" }],
|
|
187
204
|
};
|
|
@@ -205,8 +222,9 @@ export function createInboxView(deps) {
|
|
|
205
222
|
onActivate: { type: "submit", name: "open" },
|
|
206
223
|
};
|
|
207
224
|
}
|
|
208
|
-
|
|
209
|
-
|
|
225
|
+
/** The list column: the tab's collection, or the store-read error. */
|
|
226
|
+
function sidebarBlocks(tab, error) {
|
|
227
|
+
return error === undefined
|
|
210
228
|
? [collectionFor(tab)]
|
|
211
229
|
: [
|
|
212
230
|
{
|
|
@@ -217,41 +235,54 @@ export function createInboxView(deps) {
|
|
|
217
235
|
body: error,
|
|
218
236
|
},
|
|
219
237
|
];
|
|
238
|
+
}
|
|
239
|
+
/** Main with nothing selected. */
|
|
240
|
+
function placeholderMain() {
|
|
241
|
+
return [
|
|
242
|
+
{
|
|
243
|
+
type: "markdown",
|
|
244
|
+
id: "inbox-select",
|
|
245
|
+
text: "Select an escalation from the list to read it and answer.",
|
|
246
|
+
},
|
|
247
|
+
];
|
|
248
|
+
}
|
|
249
|
+
/** Main for an unknown id: identical for unknown and for invisible ids. */
|
|
250
|
+
function notFoundMain() {
|
|
251
|
+
return [
|
|
252
|
+
{
|
|
253
|
+
type: "callout",
|
|
254
|
+
id: "inbox-not-found-callout",
|
|
255
|
+
tone: "bad",
|
|
256
|
+
title: "Escalation not found",
|
|
257
|
+
},
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* The split page: tabs in the header, the list in the sidebar, the
|
|
262
|
+
* selection (or a placeholder) in main. On compact widths the
|
|
263
|
+
* `selection` drill-in shows the list first; back returns to the tab.
|
|
264
|
+
*/
|
|
265
|
+
function pageTree(tab, sidebar, main, selected) {
|
|
220
266
|
return {
|
|
221
267
|
v: 1,
|
|
222
268
|
root: {
|
|
223
269
|
type: "page",
|
|
224
270
|
id: "inbox-root",
|
|
225
271
|
title: "Inbox",
|
|
272
|
+
subtitle: TABS.find((entry) => entry.id === tab)?.label ?? "",
|
|
226
273
|
actions: TABS.map((entry) => ({
|
|
227
274
|
id: `tab-${entry.id}`,
|
|
228
275
|
label: entry.label,
|
|
229
276
|
action: { type: "navigate", params: { tab: entry.id } },
|
|
230
277
|
})),
|
|
278
|
+
sidebar,
|
|
231
279
|
main,
|
|
280
|
+
selection: { selected, back: { tab } },
|
|
232
281
|
},
|
|
233
282
|
};
|
|
234
283
|
}
|
|
235
|
-
/**
|
|
236
|
-
function
|
|
237
|
-
return {
|
|
238
|
-
v: 1,
|
|
239
|
-
root: {
|
|
240
|
-
type: "page",
|
|
241
|
-
id: "inbox-not-found",
|
|
242
|
-
title: "Inbox",
|
|
243
|
-
main: [
|
|
244
|
-
{
|
|
245
|
-
type: "callout",
|
|
246
|
-
id: "inbox-not-found-callout",
|
|
247
|
-
tone: "bad",
|
|
248
|
-
title: "Escalation not found",
|
|
249
|
-
},
|
|
250
|
-
],
|
|
251
|
-
},
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
function detailTree(item, jobName) {
|
|
284
|
+
/** Main for one selected item: header card with its actions, body, forms, history. */
|
|
285
|
+
function detailMain(item, jobName) {
|
|
255
286
|
const orphaned = item.orphaned === true;
|
|
256
287
|
const actions = [];
|
|
257
288
|
if (item.status === "open" || item.status === "answered") {
|
|
@@ -303,7 +334,16 @@ export function createInboxView(deps) {
|
|
|
303
334
|
},
|
|
304
335
|
});
|
|
305
336
|
}
|
|
306
|
-
const main = [
|
|
337
|
+
const main = [
|
|
338
|
+
{
|
|
339
|
+
type: "callout",
|
|
340
|
+
id: "inbox-item",
|
|
341
|
+
tone: "neutral",
|
|
342
|
+
title: item.title,
|
|
343
|
+
body: `${sourceLabel(item, jobName)} · ${item.status}`,
|
|
344
|
+
actions,
|
|
345
|
+
},
|
|
346
|
+
];
|
|
307
347
|
if (orphaned) {
|
|
308
348
|
main.push({
|
|
309
349
|
type: "callout",
|
|
@@ -369,17 +409,7 @@ export function createInboxView(deps) {
|
|
|
369
409
|
lines: historyLines(item),
|
|
370
410
|
wrap: true,
|
|
371
411
|
});
|
|
372
|
-
return
|
|
373
|
-
v: 1,
|
|
374
|
-
root: {
|
|
375
|
-
type: "page",
|
|
376
|
-
id: "inbox-detail",
|
|
377
|
-
title: item.title,
|
|
378
|
-
subtitle: sourceLabel(item, jobName),
|
|
379
|
-
actions,
|
|
380
|
-
main,
|
|
381
|
-
},
|
|
382
|
-
};
|
|
412
|
+
return main;
|
|
383
413
|
}
|
|
384
414
|
/** The shared action preamble: resolve the item inside the caller's buckets. */
|
|
385
415
|
async function visibleItem(ctx, payload) {
|
|
@@ -475,29 +505,33 @@ export function createInboxView(deps) {
|
|
|
475
505
|
access: "none",
|
|
476
506
|
};
|
|
477
507
|
}
|
|
478
|
-
const id = typeof ctx.params.id === "string"
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
// Nothing here can be acted on, so the page is not writable.
|
|
483
|
-
return { tree: notFoundTree(), data: {}, access: "none" };
|
|
484
|
-
}
|
|
485
|
-
const job = await jobOf(found.item);
|
|
486
|
-
return {
|
|
487
|
-
tree: detailTree(found.item, job?.name),
|
|
488
|
-
data: { answer: {}, snooze: { days: 7 } },
|
|
489
|
-
access: "write",
|
|
490
|
-
};
|
|
491
|
-
}
|
|
508
|
+
const id = typeof ctx.params.id === "string" && ctx.params.id !== ""
|
|
509
|
+
? ctx.params.id
|
|
510
|
+
: undefined;
|
|
511
|
+
const found = id === undefined ? undefined : await findVisible(store, buckets, id);
|
|
492
512
|
const tabParam = ctx.params.tab;
|
|
513
|
+
// An id with no tab (a notice or tool link) opens the list the
|
|
514
|
+
// item lives in.
|
|
493
515
|
const tab = TABS.some((entry) => entry.id === tabParam)
|
|
494
516
|
? tabParam
|
|
495
|
-
:
|
|
517
|
+
: found !== undefined
|
|
518
|
+
? tabOf(found.item.status)
|
|
519
|
+
: "open";
|
|
520
|
+
let main;
|
|
521
|
+
if (id === undefined)
|
|
522
|
+
main = placeholderMain();
|
|
523
|
+
else if (found === undefined)
|
|
524
|
+
main = notFoundMain();
|
|
525
|
+
else
|
|
526
|
+
main = detailMain(found.item, (await jobOf(found.item))?.name);
|
|
527
|
+
const selected = id !== undefined;
|
|
528
|
+
const detailData = found === undefined ? {} : { answer: {}, snooze: { days: 7 } };
|
|
496
529
|
try {
|
|
497
530
|
const rows = (await Promise.all(buckets.map(rowsFor))).flat();
|
|
498
|
-
const
|
|
531
|
+
const items = sortForTab(rows, tab).map((row) => found !== undefined && row.id === found.item.id ? { ...row, viewing: "Viewing" } : row);
|
|
532
|
+
const data = { items, ...detailData };
|
|
499
533
|
return {
|
|
500
|
-
tree:
|
|
534
|
+
tree: pageTree(tab, sidebarBlocks(tab), main, selected),
|
|
501
535
|
// SAFETY: rows are a JSON mirror (plain literals), same cast
|
|
502
536
|
// the curator view uses for its data document.
|
|
503
537
|
data: data,
|
|
@@ -506,20 +540,27 @@ export function createInboxView(deps) {
|
|
|
506
540
|
}
|
|
507
541
|
catch (error) {
|
|
508
542
|
return {
|
|
509
|
-
tree:
|
|
510
|
-
data: { error: errorText(error) },
|
|
543
|
+
tree: pageTree(tab, sidebarBlocks(tab, errorText(error)), main, selected),
|
|
544
|
+
data: { error: errorText(error), ...detailData },
|
|
511
545
|
access: "write",
|
|
512
546
|
};
|
|
513
547
|
}
|
|
514
548
|
},
|
|
515
549
|
actions: {
|
|
516
|
-
open: async (
|
|
550
|
+
open: async (ctx, payload) => {
|
|
517
551
|
const id = itemIdOf(payload);
|
|
518
552
|
if (id === undefined)
|
|
519
553
|
return notFound();
|
|
554
|
+
// Keep the list the row was opened from beside the selection.
|
|
555
|
+
const tab = TABS.some((entry) => entry.id === ctx.params.tab)
|
|
556
|
+
? ctx.params.tab
|
|
557
|
+
: undefined;
|
|
520
558
|
return {
|
|
521
559
|
ok: true,
|
|
522
|
-
navigate: {
|
|
560
|
+
navigate: {
|
|
561
|
+
type: "navigate",
|
|
562
|
+
params: tab === undefined ? { id } : { tab, id },
|
|
563
|
+
},
|
|
523
564
|
};
|
|
524
565
|
},
|
|
525
566
|
answer: async (ctx, payload) => await actOn(ctx, payload, async ({ bucket, item }) => {
|