slotlock 0.8.0 → 0.9.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +237 -0
  2. package/agent-server.d.ts +25 -5
  3. package/agent-server.js +152 -7
  4. package/agent-store-backend.d.ts +6 -1
  5. package/agent-store-backend.js +861 -55
  6. package/application-api.d.ts +19 -1
  7. package/application-api.js +578 -21
  8. package/approvals.d.ts +2 -0
  9. package/approvals.js +12 -5
  10. package/assistant.d.ts +34 -8
  11. package/assistant.js +81 -26
  12. package/availability-store.d.ts +193 -0
  13. package/availability-store.js +558 -0
  14. package/booking-rules.d.ts +92 -5
  15. package/booking-rules.js +243 -15
  16. package/dashboard-assistant-ui.d.ts +1 -1
  17. package/dashboard-assistant-ui.js +109 -8
  18. package/dashboard.d.ts +55 -6
  19. package/dashboard.js +790 -105
  20. package/ddl.d.ts +5 -1
  21. package/ddl.js +760 -9
  22. package/event-feed.d.ts +1 -1
  23. package/event-feed.js +18 -0
  24. package/holiday-import.d.ts +38 -0
  25. package/holiday-import.js +57 -0
  26. package/holidays.d.ts +38 -0
  27. package/holidays.js +150 -0
  28. package/hours.d.ts +125 -0
  29. package/hours.js +224 -0
  30. package/index.d.ts +14 -3
  31. package/index.js +10 -2
  32. package/interop.d.ts +25 -7
  33. package/interop.js +145 -17
  34. package/lifecycle.d.ts +31 -0
  35. package/lifecycle.js +115 -0
  36. package/maintenance.d.ts +19 -0
  37. package/maintenance.js +55 -1
  38. package/metrics.js +3 -0
  39. package/openapi.json +16237 -7795
  40. package/package.json +1 -1
  41. package/ranges.d.ts +28 -0
  42. package/ranges.js +135 -0
  43. package/resource-search.d.ts +19 -0
  44. package/resource-search.js +153 -0
  45. package/rest-api.js +7 -0
  46. package/rules.d.ts +2 -0
  47. package/rules.js +2 -0
  48. package/self-host.d.ts +1 -1
  49. package/self-host.js +221 -21
  50. package/slotlock.manifest.json +1 -1
  51. package/store.d.ts +367 -24
  52. package/store.js +1567 -97
  53. package/types.d.ts +198 -0
@@ -6,6 +6,7 @@ const STEP_WORDS = Object.freeze({
6
6
  slotlock_get_free_busy: ['Checking free and busy time', 'Checked free and busy time', 'check free and busy time'],
7
7
  slotlock_find_next_available: ['Finding the next free time', 'Found the next free time', 'find the next free time'],
8
8
  slotlock_find_available_slots: ['Checking availability', 'Checked availability', 'check availability'],
9
+ slotlock_find_available_ranges: ['Finding free ranges', 'Found free ranges', 'find free ranges'],
9
10
  slotlock_get_pool_availability: ['Checking the pool', 'Checked the pool', 'check the pool'],
10
11
  slotlock_get_event: ['Reading a booking', 'Read a booking', 'read the booking'],
11
12
  slotlock_list_events: ['Reading bookings', 'Read bookings', 'read bookings'],
@@ -43,6 +44,7 @@ export function slotlockAssistantActionVerb(operation) {
43
44
  case 'slotlock_delete_event':
44
45
  return { verb: 'Delete booking', busy: 'Deleting…', done: 'Deleted', danger: true };
45
46
  case 'slotlock_hold_slots':
47
+ case 'slotlock_hold_together':
46
48
  return { verb: 'Hold', busy: 'Holding…', done: 'Held', danger: false };
47
49
  case 'slotlock_release_slots':
48
50
  return { verb: 'Release', busy: 'Releasing…', done: 'Released', danger: true };
@@ -159,8 +161,59 @@ button.ghost:hover { background: var(--hover); color: var(--ink); }
159
161
  .assistant.streaming .send .stop { display: block; }
160
162
  .jump { position: absolute; left: 50%; transform: translate(-50%, 0); top: -8px; width: 32px; height: 32px; padding: 0; border-radius: 50%; border: 1px solid var(--line); background: var(--surface); color: var(--ink-2); display: grid; place-items: center; cursor: pointer; box-shadow: var(--pop-shadow); }
161
163
  .jump[hidden] { display: none; }
162
- .new-chat svg { width: 14px; height: 14px; }
164
+ .new-chat svg, .chats-link svg { width: 14px; height: 14px; }
163
165
  .assistant-page[data-empty] .new-chat { display: none; }
166
+ .assistant-actions { display: flex; gap: var(--space-2); }
167
+ .chats-link { display: none; }
168
+ /* Chats: the member's conversations beside the one shown, most recent first, grouped by day. */
169
+ .assistant-shell { display: grid; grid-template-columns: 252px minmax(0, 1fr); gap: var(--space-6); align-items: start; }
170
+ /* It scrolls with the page: a column that scrolled on its own would clip a row's menu. */
171
+ .chats { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
172
+ .chats-head { display: flex; align-items: center; justify-content: space-between; min-height: 36px; }
173
+ .chats-head h2 { margin: 0; font-size: var(--text-md); font-weight: 600; color: var(--ink-2); }
174
+ .icon-button { display: grid; place-items: center; width: 30px; height: 30px; border-radius: var(--radius-sm); color: var(--ink-2); transition: background var(--fast) var(--ease), color var(--fast) var(--ease); }
175
+ .icon-button:hover { background: var(--hover); color: var(--ink); text-decoration: none; }
176
+ .icon-button svg { width: 15px; height: 15px; }
177
+ .chats-filter { height: 32px; font-size: var(--text-md); }
178
+ .chats-filter[hidden] { display: none; }
179
+ /* Sentence case, not the dashboard's uppercase h3. */
180
+ .chat-group { margin: var(--space-3) 0 2px; padding: 0 var(--space-2); font-family: inherit; font-size: var(--text-xs); font-weight: 500; line-height: 1.5; text-transform: none; letter-spacing: 0; color: var(--ink-3); }
181
+ .chat-group[hidden] { display: none; }
182
+ .chat-list { min-width: 0; }
183
+ /* minmax(0, 1fr): a long title is cut with an ellipsis, never widening the column. */
184
+ .chat-rows { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 1px; }
185
+ .chat-row { position: relative; display: flex; align-items: center; border-radius: var(--radius); transition: background var(--fast) var(--ease); }
186
+ .chat-row[hidden] { display: none; }
187
+ .chat-row:hover, .chat-row:focus-within, .chat-row.current { background: var(--hover); }
188
+ .chat-link { flex: 1; min-width: 0; display: flex; align-items: baseline; gap: var(--space-2); padding: 7px 4px 7px var(--space-2); color: var(--ink-2); font-size: var(--text-md); }
189
+ .chat-link:hover { color: var(--ink); text-decoration: none; }
190
+ .chat-row.current .chat-link { color: var(--ink); font-weight: 500; }
191
+ .chat-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
192
+ .chat-when { flex: none; font-size: var(--text-xs); color: var(--ink-3); font-variant-numeric: tabular-nums; }
193
+ .chat-menu { position: relative; flex: none; }
194
+ .chat-menu summary { list-style: none; display: grid; place-items: center; width: 28px; height: 28px; margin-right: 2px; border-radius: var(--radius-sm); color: var(--ink-3); cursor: pointer; opacity: 0; transition: opacity var(--fast) var(--ease), background var(--fast) var(--ease); }
195
+ .chat-menu summary::-webkit-details-marker { display: none; }
196
+ .chat-row:hover .chat-menu summary, .chat-row:focus-within .chat-menu summary, .chat-row.current .chat-menu summary, .chat-menu[open] summary { opacity: 1; }
197
+ .chat-menu summary:hover, .chat-menu[open] summary { background: var(--line-soft); color: var(--ink); }
198
+ .chat-menu summary svg { width: 14px; height: 14px; }
199
+ .chat-menu .pop { z-index: 20; width: 260px; display: grid; gap: var(--space-2); }
200
+ .chat-menu .pop form { display: grid; gap: var(--space-2); margin: 0; }
201
+ .chat-menu .pop form + form { padding-top: var(--space-2); border-top: 1px solid var(--line-soft); }
202
+ .chat-menu .pop button.danger { background: none; border-color: transparent; box-shadow: none; color: var(--bad); }
203
+ .chat-menu .pop button.danger:hover { background: var(--bad-wash); }
204
+ .chat-rename input { width: 100%; min-width: 0; height: 34px; }
205
+ .chats-empty { padding: var(--space-2); font-size: var(--text-md); }
206
+ .chats-page .chats { max-width: 560px; }
207
+ .chats-page .chats-head { display: none; }
208
+ .chats-page .chat-menu summary { opacity: 1; }
209
+ @media (max-width: 1100px) {
210
+ .assistant-shell { grid-template-columns: minmax(0, 1fr); }
211
+ .assistant-shell > .chats { display: none; }
212
+ .chats-link { display: inline-flex; }
213
+ }
214
+ @media (hover: none) {
215
+ .chat-menu summary { opacity: 1; }
216
+ }
164
217
  @media (prefers-reduced-motion: reduce) {
165
218
  .live-steps .step.run, .thinking { animation: none; color: var(--ink-3); background: none; }
166
219
  .prose.streaming > :last-child::after { animation: none; }
@@ -198,6 +251,29 @@ button.ghost:hover { background: var(--hover); color: var(--ink); }
198
251
  */
199
252
  export function slotlockAssistantScript(markdownSource) {
200
253
  return `
254
+ // Search chats: rows whose title holds what is typed (ignoring case and accents) stay shown.
255
+ function filterChats() {
256
+ var filter = document.getElementById('chats-filter');
257
+ if (!filter) return;
258
+ filter.hidden = false;
259
+ var fold = function (text) { return text.normalize('NFKD').replace(/\\p{M}/gu, '').toLowerCase(); };
260
+ var apply = function () {
261
+ var wanted = fold(filter.value.trim());
262
+ for (var group of document.querySelectorAll('.chat-rows')) {
263
+ var shown = 0;
264
+ for (var row of group.children) {
265
+ var title = row.querySelector('.chat-title');
266
+ row.hidden = wanted !== '' && fold(title ? title.textContent : '').indexOf(wanted) === -1;
267
+ if (!row.hidden) shown += 1;
268
+ }
269
+ var label = group.previousElementSibling;
270
+ if (label && label.classList.contains('chat-group')) label.hidden = shown === 0;
271
+ }
272
+ };
273
+ filter.addEventListener('input', apply);
274
+ apply();
275
+ }
276
+ filterChats();
201
277
  (function () {
202
278
  var page = document.querySelector('[data-assistant]');
203
279
  if (!page || typeof fetch !== 'function' || typeof TextDecoder !== 'function') return;
@@ -267,14 +343,20 @@ export function slotlockAssistantScript(markdownSource) {
267
343
  event.preventDefault();
268
344
  if (!busy && box.value.trim() !== '') form.requestSubmit();
269
345
  });
270
- for (var chip of page.querySelectorAll('button[data-fill]')) {
346
+ // A chip that asks sends through the box, so its reply streams; one that writes only fills it.
347
+ for (var chip of page.querySelectorAll('.chips button.chip')) {
271
348
  chip.addEventListener('click', function (event) {
272
349
  event.preventDefault();
273
- box.value = this.getAttribute('data-fill') || '';
350
+ var fill = this.getAttribute('data-fill');
351
+ box.value = fill !== null ? fill : (this.value || '');
274
352
  grow();
275
353
  update();
276
- box.focus();
277
- box.setSelectionRange(box.value.length, box.value.length);
354
+ if (fill !== null) {
355
+ box.focus();
356
+ box.setSelectionRange(box.value.length, box.value.length);
357
+ } else if (!busy && box.value.trim() !== '') {
358
+ form.requestSubmit();
359
+ }
278
360
  });
279
361
  }
280
362
  for (var decide of document.querySelectorAll('.change-buttons form')) {
@@ -323,7 +405,8 @@ export function slotlockAssistantScript(markdownSource) {
323
405
  var prose = null;
324
406
  var proseText = '';
325
407
  var steps = null;
326
- var stepNodes = {};
408
+ // Keyed by the provider's call id: no id, __proto__ included, may name an inherited property.
409
+ var stepNodes = Object.create(null);
327
410
  var frame = 0;
328
411
  follow(was);
329
412
  function draw() {
@@ -374,6 +457,18 @@ export function slotlockAssistantScript(markdownSource) {
374
457
  added = [];
375
458
  thread.insertAdjacentHTML('beforeend', e.html || '');
376
459
  setOnce(e.once);
460
+ // The conversation it went to: a new one has its id now, its own address, and a place in Chats.
461
+ if (e.chat) {
462
+ for (var field of page.querySelectorAll('input[name="chat"]')) field.value = e.chat;
463
+ if (e.url && window.history && history.replaceState) history.replaceState(null, '', e.url);
464
+ var heading = document.querySelector('.assistant-page .page-head h1');
465
+ if (heading && e.title) heading.textContent = e.title;
466
+ var panel = document.querySelector('aside.chats');
467
+ if (panel && e.chats) {
468
+ panel.outerHTML = e.chats;
469
+ filterChats();
470
+ }
471
+ }
377
472
  }
378
473
  follow(was);
379
474
  }
@@ -386,7 +481,8 @@ export function slotlockAssistantScript(markdownSource) {
386
481
  }).then(function (response) {
387
482
  var type = response.headers.get('content-type') || '';
388
483
  if (!response.body || type.indexOf('application/x-ndjson') !== 0) {
389
- location.assign(page.getAttribute('data-page') + '#latest');
484
+ var current = form.querySelector('input[name="chat"]');
485
+ location.assign(page.getAttribute('data-page') + (current && current.value ? '?c=' + current.value : '') + '#latest');
390
486
  location.reload();
391
487
  return;
392
488
  }
@@ -432,7 +528,12 @@ export function slotlockAssistantScript(markdownSource) {
432
528
  });
433
529
  grow();
434
530
  update();
435
- if (location.hash === '#latest' || !page.hasAttribute('data-empty')) window.scrollTo(0, document.documentElement.scrollHeight);
531
+ // To the latest message only when it would be behind the box: a short conversation keeps its
532
+ // heading (and Chats) in view.
533
+ var dockAtLoad = page.querySelector('.dock');
534
+ var lastAtLoad = thread.lastElementChild;
535
+ var behind = !!lastAtLoad && lastAtLoad.getBoundingClientRect().bottom > window.innerHeight - (dockAtLoad ? dockAtLoad.offsetHeight : 0);
536
+ if (!page.hasAttribute('data-empty') && behind) window.scrollTo(0, document.documentElement.scrollHeight);
436
537
  })();
437
538
  `;
438
539
  }
package/dashboard.d.ts CHANGED
@@ -9,8 +9,11 @@ import { type SlotlockLocation, type SlotlockResourceGroup, type SlotlockResourc
9
9
  import { type SlotlockUsageRow } from './usage.js';
10
10
  import { type CreateWebhookEndpointResult, type SlotlockChange, type SlotlockWebhookEndpoint } from './event-feed.js';
11
11
  import type { SlotlockSql, SlotlockStore, SlotlockWaitlistEntry } from './store.js';
12
+ import type { SlotlockAvailabilityDefaults, SlotlockAvailabilityExceptionInput, SlotlockHolidayCalendar, SlotlockOverrideResources } from './availability-store.js';
13
+ import { type SlotlockAvailabilityException } from './hours.js';
14
+ import { type SlotlockHolidaySource } from './holiday-import.js';
12
15
  import { type SlotlockSandboxScenario } from './sandbox.js';
13
- import type { Interval, SlotlockAgendaEntry, SlotlockApproval, SlotlockResource, SlotlockBookingRuleDefaults, WeeklyAvailabilityRule } from './types.js';
16
+ import type { Interval, SlotlockAgendaEntry, SlotlockApproval, SlotlockContact, SlotlockResource, SlotlockBookingRuleDefaults, WeeklyAvailabilityRule } from './types.js';
14
17
  /** Resources one dashboard tenant may hold. */
15
18
  export declare const SLOTLOCK_DASHBOARD_MAX_RESOURCES = 500;
16
19
  /**
@@ -30,13 +33,51 @@ export interface SlotlockDashboardResources {
30
33
  */
31
34
  add(tenantRef: string, externalRef: string, timezone: string, actor?: string): Promise<SlotlockResource>;
32
35
  /**
33
- * Set the bookable hours of one of the tenant's resources (`{ id }`) or of all of them; `null`
34
- * hands them back to the server's and `[]` closes. Returns how many resources changed: 0 for an
35
- * id that is not the tenant's.
36
+ * Set the bookable hours of one of the tenant's resources (`{ id }`), one of its locations
37
+ * (`{ location }`), or the tenant's own (`'all'`), which every resource without its own inherits
38
+ * (RFC 0004 B2); `null` hands them to what is above and `[]` closes. For the tenant's, `override`
39
+ * says what becomes of resources' own hours (SlotlockOverrideResources: `none` keeps them).
40
+ * Returns 0 for an id or location that is not the tenant's, else at least 1 (for the tenant's, how
41
+ * many resources they now apply to).
36
42
  */
37
43
  setHours(tenantRef: string, target: {
38
44
  id: string;
39
- } | 'all', rules: WeeklyAvailabilityRule[] | null, actor?: string): Promise<number>;
45
+ } | {
46
+ location: string;
47
+ } | 'all', rules: WeeklyAvailabilityRule[] | null, actor?: string, override?: SlotlockOverrideResources): Promise<number>;
48
+ /** The people the tenant keeps to tell about bookings (RFC 0004 P3), and forgetting one. */
49
+ contacts?: {
50
+ list(tenantRef: string, limit: number, after?: string): Promise<SlotlockContact[]>;
51
+ forget(tenantRef: string, ref: string, actor?: string): Promise<{
52
+ deleted: boolean;
53
+ bookings: number;
54
+ }>;
55
+ };
56
+ /** The tenant's stored hours and its closures and special hours (RFC 0004 B1, B2). */
57
+ hours?: {
58
+ defaults(tenantRef: string): Promise<SlotlockAvailabilityDefaults>;
59
+ exceptions(tenantRef: string, limit: number): Promise<SlotlockAvailabilityException[]>;
60
+ addException(tenantRef: string, exception: Omit<SlotlockAvailabilityExceptionInput, 'tenantRef'>, actor?: string): Promise<SlotlockAvailabilityException>;
61
+ deleteException(tenantRef: string, id: string, actor?: string): Promise<boolean>;
62
+ /**
63
+ * Import a holiday calendar (its text, or a URL with `byUrl`): each all-day event closes its
64
+ * days for the next two years. Kept by name at its scope, so importing it again updates it and
65
+ * removes the holidays it dropped; `subscribe` imports a URL again every 30 days.
66
+ */
67
+ importHolidays(tenantRef: string, source: SlotlockHolidaySource, scope: {
68
+ location?: string;
69
+ resourceId?: string;
70
+ }, actor?: string): Promise<{
71
+ imported: number;
72
+ skipped: number;
73
+ removed: number;
74
+ }>;
75
+ /** Whether importHolidays reads calendars by URL. */
76
+ byUrl?: boolean;
77
+ /** The tenant's holiday calendars, and deleting one with its closures (false: not the tenant's). */
78
+ calendars?(tenantRef: string): Promise<SlotlockHolidayCalendar[]>;
79
+ deleteCalendar?(tenantRef: string, id: string, actor?: string): Promise<boolean>;
80
+ };
40
81
  /** What occupies the tenant's resources in `window`, by start: the overview's agenda. */
41
82
  agenda?(tenantRef: string, window: Interval, limit: number): Promise<SlotlockAgendaEntry[]>;
42
83
  /** The tenant's latest changes, newest first: the overview's activity. */
@@ -229,7 +270,10 @@ export interface SlotlockDashboardAssistantOptions {
229
270
  turnTimeoutMs?: number;
230
271
  /** Model calls one message may make (8 by default, at most 20). */
231
272
  maxSteps?: number;
232
- /** How long a conversation is kept after its last message: 2 hours by default. */
273
+ /**
274
+ * How long a conversation is kept after its last message (1 second to 3,650 days). Without it, a
275
+ * member keeps each conversation until they delete it, up to SLOTLOCK_ASSISTANT_MAX_CHATS.
276
+ */
233
277
  conversationTtlMs?: number;
234
278
  }
235
279
  /**
@@ -424,6 +468,11 @@ export declare function createSlotlockDashboardResources(store: SlotlockStore, o
424
468
  * through: with it, the dashboard lists approvals and decides them.
425
469
  */
426
470
  agentBackend?: SlotlockAgentCalendarBackend;
471
+ /**
472
+ * Fetches holiday calendars by URL (the guarded fetch under `serve`): with it, Closures imports
473
+ * and subscribes to a calendar's address.
474
+ */
475
+ holidayFetch?: typeof fetch;
427
476
  }): SlotlockDashboardResources;
428
477
  /**
429
478
  * Records accepted write confirmations (`SlotlockAgentConfirmationOptions.consumeAcceptance`) in