@things-factory/ai-assistant 10.1.213 → 10.1.215

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 (89) hide show
  1. package/client/components/assistant-dock.ts +6 -3
  2. package/client/components/assistant-selection-bar.test.ts +42 -0
  3. package/client/components/assistant-selection-bar.ts +129 -0
  4. package/client/components/assistant-session-manager.ts +263 -12
  5. package/client/index.ts +2 -0
  6. package/client/utils/assistant-selection.test.ts +25 -1
  7. package/client/utils/assistant-selection.ts +27 -0
  8. package/client/utils/assistant-session-actions.ts +12 -0
  9. package/client/utils/assistant-session-admin.ts +70 -5
  10. package/dist-client/components/assistant-dock.js +6 -3
  11. package/dist-client/components/assistant-dock.js.map +1 -1
  12. package/dist-client/components/assistant-selection-bar.d.ts +44 -0
  13. package/dist-client/components/assistant-selection-bar.js +149 -0
  14. package/dist-client/components/assistant-selection-bar.js.map +1 -0
  15. package/dist-client/components/assistant-selection-bar.test.d.ts +1 -0
  16. package/dist-client/components/assistant-selection-bar.test.js +38 -0
  17. package/dist-client/components/assistant-selection-bar.test.js.map +1 -0
  18. package/dist-client/components/assistant-session-manager.d.ts +37 -0
  19. package/dist-client/components/assistant-session-manager.js +281 -13
  20. package/dist-client/components/assistant-session-manager.js.map +1 -1
  21. package/dist-client/index.d.ts +1 -0
  22. package/dist-client/index.js +2 -0
  23. package/dist-client/index.js.map +1 -1
  24. package/dist-client/tsconfig.tsbuildinfo +1 -1
  25. package/dist-client/utils/assistant-selection.d.ts +24 -0
  26. package/dist-client/utils/assistant-selection.js +24 -0
  27. package/dist-client/utils/assistant-selection.js.map +1 -1
  28. package/dist-client/utils/assistant-selection.test.js +21 -1
  29. package/dist-client/utils/assistant-selection.test.js.map +1 -1
  30. package/dist-client/utils/assistant-session-actions.d.ts +15 -0
  31. package/dist-client/utils/assistant-session-actions.js +11 -0
  32. package/dist-client/utils/assistant-session-actions.js.map +1 -1
  33. package/dist-client/utils/assistant-session-admin.d.ts +32 -1
  34. package/dist-client/utils/assistant-session-admin.js +36 -5
  35. package/dist-client/utils/assistant-session-admin.js.map +1 -1
  36. package/dist-server/service/assistant-chat-resolver.js +6 -4
  37. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  38. package/dist-server/service/assistant-mode/index.d.ts +1 -1
  39. package/dist-server/service/assistant-mode/index.js +2 -1
  40. package/dist-server/service/assistant-mode/index.js.map +1 -1
  41. package/dist-server/service/assistant-mode/mode-categories.d.ts +30 -0
  42. package/dist-server/service/assistant-mode/mode-categories.js +35 -2
  43. package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
  44. package/dist-server/service/chat-session/session-cleanup.d.ts +28 -0
  45. package/dist-server/service/chat-session/session-cleanup.js +69 -0
  46. package/dist-server/service/chat-session/session-cleanup.js.map +1 -0
  47. package/dist-server/service/chat-session/session-inbox.d.ts +15 -0
  48. package/dist-server/service/chat-session/session-inbox.js +11 -0
  49. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  50. package/dist-server/service/chat-session/session-purge.d.ts +4 -0
  51. package/dist-server/service/chat-session/session-purge.js +45 -11
  52. package/dist-server/service/chat-session/session-purge.js.map +1 -1
  53. package/dist-server/service/chat-session-resolver.d.ts +24 -2
  54. package/dist-server/service/chat-session-resolver.js +193 -13
  55. package/dist-server/service/chat-session-resolver.js.map +1 -1
  56. package/dist-server/service/data-read/list-read.d.ts +3 -1
  57. package/dist-server/service/data-read/list-read.js +27 -2
  58. package/dist-server/service/data-read/list-read.js.map +1 -1
  59. package/dist-server/service/data-read/read-target.d.ts +16 -0
  60. package/dist-server/service/data-read/read-target.js +18 -1
  61. package/dist-server/service/data-read/read-target.js.map +1 -1
  62. package/dist-server/service/data-read/read-tools.js +3 -1
  63. package/dist-server/service/data-read/read-tools.js.map +1 -1
  64. package/dist-server/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +6 -6
  66. package/server/service/assistant-chat-resolver.ts +7 -5
  67. package/server/service/assistant-mode/conversation-category.test.ts +15 -2
  68. package/server/service/assistant-mode/index.ts +1 -1
  69. package/server/service/assistant-mode/mode-categories.ts +48 -2
  70. package/server/service/assistant-mode/mode-front.test.ts +27 -1
  71. package/server/service/assistant-mode/open-subject-door.test.ts +3 -1
  72. package/server/service/chat-session/session-cleanup.test.ts +83 -0
  73. package/server/service/chat-session/session-cleanup.ts +69 -0
  74. package/server/service/chat-session/session-inbox.ts +13 -0
  75. package/server/service/chat-session/session-purge.ts +46 -10
  76. package/server/service/chat-session-resolver.ts +150 -14
  77. package/server/service/data-read/list-read.ts +30 -3
  78. package/server/service/data-read/list-status.test.ts +46 -0
  79. package/server/service/data-read/read-target.ts +32 -1
  80. package/server/service/data-read/read-tools.ts +2 -1
  81. package/test/assistant-dock.test.ts +12 -1
  82. package/test/assistant-session-manager.test.ts +53 -1
  83. package/test/open-doors.test.ts +3 -1
  84. package/translations/en.json +45 -2
  85. package/translations/es.json +45 -2
  86. package/translations/ja.json +45 -2
  87. package/translations/ko.json +45 -2
  88. package/translations/ms.json +45 -2
  89. package/translations/zh.json +45 -2
@@ -6,7 +6,7 @@ import { i18next } from '@operato/i18n';
6
6
  import { OxPrompt } from '@operato/popup';
7
7
  import '@operato/popup/ox-popup-menu.js';
8
8
  import { OxPopupMenu } from '@operato/popup/ox-popup-menu.js';
9
- import { assistantSessionAdmin, sessionActions } from '../utils/assistant-session-admin.js';
9
+ import { assistantSessionAdmin, bulkActions, sessionActions } from '../utils/assistant-session-admin.js';
10
10
  const PAGE = 30;
11
11
  const t = (key, defaultValue, options = {}) => i18next.t(`ai-assistant.session.${key}`, { defaultValue, ...options });
12
12
  /**
@@ -32,6 +32,10 @@ const t = (key, defaultValue, options = {}) => i18next.t(`ai-assistant.session.$
32
32
  * ── Asking before what cannot be undone ─────────────────────────────────────────
33
33
  * Delete, and opening a private conversation to the domain, ask on the row itself - [Cancel][OK].
34
34
  * Making it private, hiding and renaming do not: each can be undone from the same row.
35
+ *
36
+ * ── Many at once (2026-10-25, the user: 「멀티셀렉트로 한번에 지울 수 있게」) ──────────
37
+ * Each row has a box; the picked ones are deleted, left or hidden together, each by its own row's hints
38
+ * (`bulkActions`), with one question. A deleted one is kept for `deletedDays` and comes back from 「Deleted」.
35
39
  */
36
40
  let AssistantSessionManager = class AssistantSessionManager extends LitElement {
37
41
  constructor() {
@@ -57,6 +61,18 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
57
61
  this.loading = false;
58
62
  this.error = '';
59
63
  this.renamingId = '';
64
+ this.view = 'list';
65
+ this.archivedCount = 0;
66
+ this.cleanupCount = 0;
67
+ this.keeping = { deletedDays: null, archiveDays: null };
68
+ this.deleted = [];
69
+ /** The rows picked for doing at once - ids, kept across a reload for the rows still listed. */
70
+ this.picked = [];
71
+ this.busy = false;
72
+ /** What was just done, said once - and the conversation brought back, to open it from here. */
73
+ this.notice = null;
74
+ /** The suggestions arrive picked - set when the view turns to them, spent by the first read. */
75
+ this.pickAllOnRead = false;
60
76
  this.generation = 0;
61
77
  }
62
78
  static { this.styles = css `
@@ -305,6 +321,38 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
305
321
  .hiddenfilter md-icon {
306
322
  --md-icon-size: 16px;
307
323
  }
324
+
325
+ .pick { margin: 0; cursor: pointer; }
326
+ /* The box is small; the space around it picks too - a press beside it must not open the conversation. */
327
+ .pick-area { flex: none; display: inline-flex; align-items: center; align-self: stretch; padding: 0 8px; margin: -7px -8px -7px -10px; padding-left: 10px; cursor: pointer; }
328
+ .pickbar {
329
+ display: flex;
330
+ flex-wrap: wrap;
331
+ align-items: center;
332
+ gap: 4px 10px;
333
+ padding: 6px 10px;
334
+ border-bottom: 1px solid var(--md-sys-color-outline-variant);
335
+ font-size: 11px;
336
+ color: var(--md-sys-color-on-surface-variant);
337
+ }
338
+ .pickbar label { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; }
339
+ .pickbar .act,
340
+ .suggest,
341
+ .note .open-link,
342
+ .restore {
343
+ border: 0;
344
+ background: none;
345
+ padding: 0;
346
+ color: var(--md-sys-color-primary);
347
+ font: inherit;
348
+ cursor: pointer;
349
+ }
350
+ .pickbar .act:disabled { color: var(--md-sys-color-on-surface-variant); cursor: default; }
351
+ .pickbar .act.danger { color: var(--md-sys-color-error); }
352
+ .pickbar .unpick { margin-left: auto; }
353
+ .suggest { display: block; width: 100%; padding: 8px 10px; text-align: start; font-size: 12px; border-bottom: 1px solid var(--md-sys-color-outline-variant); }
354
+ .restore { flex: none; font-size: 12px; }
355
+ .row.deleted { cursor: default; }
308
356
  `; }
309
357
  /*
310
358
  * The search and the filters stand open together, always (2026-10-12) - a filter behind a "Filters" button is one
@@ -312,7 +360,7 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
312
360
  * to choose.
313
361
  */
314
362
  get filtering() {
315
- return !!this.search.trim() || this.whose !== 'all' || this.visibility !== 'all' || this.includeHidden || this.unreadOnly;
363
+ return !!this.search.trim() || this.whose !== 'all' || this.visibility !== 'all' || this.includeHidden || this.unreadOnly || this.view !== 'list';
316
364
  }
317
365
  clearFilters() {
318
366
  clearTimeout(this.searchTimer);
@@ -321,6 +369,7 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
321
369
  this.whose = 'all';
322
370
  this.visibility = 'all';
323
371
  this.includeHidden = this.unreadOnly = false;
372
+ this.view = 'list';
324
373
  });
325
374
  }
326
375
  /** Read the list again - the host calls this after it opened or started a conversation. */
@@ -334,6 +383,18 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
334
383
  this.loading = true;
335
384
  this.error = '';
336
385
  try {
386
+ if (this.view === 'deleted') {
387
+ /* The deleted ones are few (kept for deletedDays) - the search narrows them by name here. */
388
+ const word = this.search.trim().toLowerCase();
389
+ const deleted = (await this.admin.deleted(this.anchorType, this.anchorId)).filter(one => !word || (one.name ?? '').toLowerCase().includes(word));
390
+ if (generation !== this.generation)
391
+ return;
392
+ this.deleted = deleted;
393
+ this.entries = [];
394
+ this.total = deleted.length;
395
+ this.keepPicked(deleted.map(one => one.id));
396
+ return;
397
+ }
337
398
  /*
338
399
  * Paging reads again from the start rather than appending - live activity reorders the list, and
339
400
  * appending a later page to an earlier order duplicates and drops rows.
@@ -347,7 +408,9 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
347
408
  whose: this.whose,
348
409
  visibility: this.visibility,
349
410
  includeHidden: this.includeHidden,
350
- unreadOnly: this.unreadOnly
411
+ unreadOnly: this.unreadOnly,
412
+ includeArchived: this.view === 'archived',
413
+ cleanupOnly: this.view === 'cleanup'
351
414
  },
352
415
  limit: this.pageSize,
353
416
  offset: 0
@@ -357,6 +420,10 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
357
420
  this.entries = page.entries;
358
421
  this.total = page.total;
359
422
  this.hiddenCount = page.hiddenCount;
423
+ this.archivedCount = page.archivedCount;
424
+ this.cleanupCount = page.cleanupCount;
425
+ this.keeping = page.keeping;
426
+ this.keepPicked(page.entries.map(entry => entry.id));
360
427
  }
361
428
  catch (error) {
362
429
  if (generation !== this.generation)
@@ -370,6 +437,26 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
370
437
  this.loading = false;
371
438
  }
372
439
  }
440
+ /** After a read: the suggestions arrive all picked; otherwise only the picked rows still listed stay picked. */
441
+ keepPicked(listed) {
442
+ if (this.pickAllOnRead) {
443
+ this.pickAllOnRead = false;
444
+ this.picked = listed;
445
+ }
446
+ else
447
+ this.picked = this.picked.filter(id => listed.includes(id));
448
+ }
449
+ setView(view) {
450
+ this.refilter(() => {
451
+ this.view = view;
452
+ this.picked = [];
453
+ this.notice = null;
454
+ this.pickAllOnRead = view === 'cleanup';
455
+ });
456
+ }
457
+ togglePick(id, on) {
458
+ this.picked = on ? [...new Set([...this.picked, id])] : this.picked.filter(one => one !== id);
459
+ }
373
460
  updated(changed) {
374
461
  if (changed.has('anchorType') || changed.has('anchorId') || changed.has('admin') || changed.has('kind'))
375
462
  void this.reload();
@@ -414,6 +501,7 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
414
501
  /** Runs one door, then reads the list again - on success and on failure alike, so the row shows the truth. */
415
502
  async act(entry, change, run, refused) {
416
503
  this.renamingId = '';
504
+ this.notice = null;
417
505
  try {
418
506
  if (!(await run()))
419
507
  this.error = refused;
@@ -445,6 +533,69 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
445
533
  _removeSession(entry) {
446
534
  void this.act(entry, 'removed', () => this.admin.remove(entry.id), t('remove-refused', 'this conversation could not be deleted.'));
447
535
  }
536
+ /**
537
+ * Runs one door for each picked row, then reads the list again. Every success is told to the host like a row's own
538
+ * change, so it moves off an open conversation that went; every refusal is said by name.
539
+ */
540
+ async runMany(ids, change, run, done) {
541
+ if (!ids.length || this.busy)
542
+ return;
543
+ this.busy = true;
544
+ this.error = '';
545
+ this.notice = null;
546
+ const names = new Map([...this.entries.map(entry => [entry.id, entry.name]), ...this.deleted.map(one => [one.id, one.name])]);
547
+ const refused = [];
548
+ let count = 0;
549
+ for (const id of ids) {
550
+ try {
551
+ if (await run(id)) {
552
+ count++;
553
+ this.emit('assistant-session-changed', { sessionId: id, change });
554
+ }
555
+ else
556
+ refused.push(names.get(id) || t('untitled', 'Untitled conversation'));
557
+ }
558
+ catch (error) {
559
+ refused.push(`${names.get(id) || t('untitled', 'Untitled conversation')} (${error.message})`);
560
+ }
561
+ }
562
+ this.picked = [];
563
+ this.busy = false;
564
+ const error = refused.length ? t('some-refused', 'Not done for {count}: {names}', { count: refused.length, names: refused.join(', ') }) : '';
565
+ await this.reload();
566
+ if (count)
567
+ this.notice = { text: done(count) };
568
+ if (error)
569
+ this.error = error;
570
+ }
571
+ /** The sentence that says a deleted conversation can still come back - from 「Deleted」, for as long as it is kept. */
572
+ keptFor() {
573
+ return this.keeping.deletedDays
574
+ ? t('kept-for', 'You can bring it back from Deleted for {days} days.', { days: this.keeping.deletedDays })
575
+ : t('kept', 'You can bring it back from Deleted.');
576
+ }
577
+ async askRemoveMany(ids) {
578
+ const ok = await this.ask(t('remove', 'Delete'), `${t('confirm-remove-many', 'Delete {count} conversations? They disappear for every participant.', { count: ids.length })} ${this.keptFor()}`, t('remove', 'Delete'));
579
+ if (ok)
580
+ void this.runMany(ids, 'removed', id => this.admin.remove(id), count => t('removed-many', '{count} deleted.', { count }));
581
+ }
582
+ async askLeaveMany(ids) {
583
+ const ok = await this.ask(t('leave', 'Leave'), t('confirm-leave-many', 'Leave {count} conversations? They stop coming to your list, and a group or private one is read again only when its owner brings you back.', { count: ids.length }), t('leave', 'Leave'));
584
+ if (ok)
585
+ void this.runMany(ids, 'left', id => this.admin.leave(id), count => t('left-many', 'Left {count}.', { count }));
586
+ }
587
+ hideMany(ids) {
588
+ void this.runMany(ids, 'hidden', id => this.admin.hide(id, true), count => t('hidden-many', '{count} hidden from your list.', { count }));
589
+ }
590
+ restoreMany(ids) {
591
+ void this.runMany(ids, 'restored', id => this.admin.restore(id), count => t('restored-many', '{count} brought back.', { count }));
592
+ }
593
+ async restoreOne(one) {
594
+ const name = one.name || t('untitled', 'Untitled conversation');
595
+ await this.runMany([one.id], 'restored', id => this.admin.restore(id), () => t('restored', '"{name}" is back.', { name }));
596
+ if (this.notice)
597
+ this.notice = { ...this.notice, open: { sessionId: one.id, kind: one.kind } };
598
+ }
448
599
  /* ── drawing ─────────────────────────────────────────────────────────────────────── */
449
600
  render() {
450
601
  const shown = this.entries.length;
@@ -463,7 +614,7 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
463
614
  <button class="new" @click=${() => this.emit('assistant-session-reset')}>+ ${t('new', 'New conversation')}</button>
464
615
  </div>
465
616
  <div class="filters">
466
- <select aria-label=${t('filter-whose', 'Whose')} .value=${this.whose}
617
+ ${this.view === 'deleted' ? nothing : html `<select aria-label=${t('filter-whose', 'Whose')} .value=${this.whose}
467
618
  @change=${(e) => this.refilter(() => (this.whose = e.target.value))}>
468
619
  <option value="all">${t('filter-all', 'All')}</option>
469
620
  <option value="mine">${t('filter-mine', 'Started by me')}</option>
@@ -475,31 +626,114 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
475
626
  <option value="open">${t('filter-open', 'Public')}</option>
476
627
  <option value="group">${t('filter-group', 'Group')}</option>
477
628
  <option value="private">${t('filter-private', 'Private')}</option>
629
+ </select>`}
630
+ <select aria-label=${t('filter-view', 'Show')} .value=${this.view}
631
+ @change=${(e) => this.setView(e.target.value)}>
632
+ <option value="list">${t('view-list', 'Active')}</option>
633
+ <option value="archived">${t('view-archived', 'Including archived')}</option>
634
+ <option value="cleanup">${t('view-cleanup', 'Suggested to clear')}</option>
635
+ <option value="deleted">${t('view-deleted', 'Deleted')}</option>
478
636
  </select>
479
- <label><input type="checkbox" .checked=${this.unreadOnly}
637
+ ${this.view === 'deleted' ? nothing : html `<label><input type="checkbox" .checked=${this.unreadOnly}
480
638
  @change=${(e) => this.refilter(() => (this.unreadOnly = e.target.checked))} />${t('filter-unread', 'Unread')}</label>
481
639
  <label><input type="checkbox" .checked=${this.includeHidden}
482
- @change=${(e) => this.refilter(() => (this.includeHidden = e.target.checked))} />${t('filter-hidden', 'Including hidden')}</label>
640
+ @change=${(e) => this.refilter(() => (this.includeHidden = e.target.checked))} />${t('filter-hidden', 'Including hidden')}</label>`}
483
641
  ${this.filtering ? html `<button class="clear" @click=${() => this.clearFilters()}>${t('reset-filters', 'Clear search and filters')}</button>` : nothing}
484
642
  </div>
643
+ ${this.renderPickBar()}
485
644
  <div class="list">
486
645
  ${this.error ? html `<div class="note" role="alert">${this.error}</div>` : nothing}
646
+ ${this.notice
647
+ ? html `<div class="note" role="status">${this.notice.text}${this.notice.open
648
+ ? html ` <button class="open-link" @click=${() => this.emit('assistant-session-select', this.notice.open)}>${t('open', 'Open')}</button>`
649
+ : nothing}</div>`
650
+ : nothing}
487
651
  ${this.loading ? html `<div class="note" role="status">${t('loading', 'Loading conversations…')}</div>` : nothing}
488
- ${shown === 0 && !this.loading && !this.error
489
- ? html `<div class="empty" role="status">${this.filtering ? t('filtered-empty', 'No conversations match these filters here.') : t('scope-empty', 'No conversations yet.')}</div>`
490
- : this.entries.map(entry => this.renderRow(entry))}
491
- ${shown < this.total
652
+ ${this.view === 'list' && this.cleanupCount > 0 && !this.search.trim()
653
+ ? html `<button class="suggest" @click=${() => this.setView('cleanup')}>${t('cleanup-suggest', '{count} of yours look ready to clear - see them', { count: this.cleanupCount })}</button>`
654
+ : nothing}
655
+ ${this.view === 'deleted'
656
+ ? this.deleted.length === 0 && !this.loading && !this.error
657
+ ? html `<div class="empty" role="status">${t('deleted-empty', 'Nothing you deleted here is kept.')}</div>`
658
+ : this.deleted.map(one => this.renderDeletedRow(one))
659
+ : shown === 0 && !this.loading && !this.error
660
+ ? html `<div class="empty" role="status">${this.view === 'cleanup'
661
+ ? t('cleanup-empty', 'Nothing of yours to clear here.')
662
+ : this.filtering ? t('filtered-empty', 'No conversations match these filters here.') : t('scope-empty', 'No conversations yet.')}</div>`
663
+ : this.entries.map(entry => this.renderRow(entry))}
664
+ ${this.view !== 'deleted' && shown < this.total
492
665
  ? html `<button class="more" @click=${() => this.loadMore()}>${t('load-more', 'Show more ({rest})', { rest: this.total - shown })}</button>`
493
666
  : nothing}
494
667
  </div>
495
668
  <!-- A hidden conversation must not be forgotten: say how many, and one press shows them. -->
496
- ${this.hiddenCount > 0 && !this.includeHidden
669
+ ${this.hiddenCount > 0 && !this.includeHidden && (this.view === 'list' || this.view === 'archived')
497
670
  ? html `<button class="hiddenfilter" @click=${() => this.refilter(() => (this.includeHidden = true))}>
498
671
  <md-icon>visibility_off</md-icon>${t('hidden-count', '{count} hidden', { count: this.hiddenCount })}
499
672
  </button>`
500
673
  : nothing}
674
+ ${this.archivedCount > 0 && this.view === 'list'
675
+ ? html `<button class="hiddenfilter" @click=${() => this.setView('archived')}>
676
+ <md-icon>inventory_2</md-icon>${t('archived-count', '{count} archived - no activity for {days} days', { count: this.archivedCount, days: this.keeping.archiveDays ?? '' })}
677
+ </button>`
678
+ : nothing}
501
679
  `;
502
680
  }
681
+ /** The box over the list: pick all, and what the picked ones can have done to them together. */
682
+ renderPickBar() {
683
+ const rows = this.view === 'deleted' ? this.deleted : this.entries;
684
+ if (!rows.length)
685
+ return nothing;
686
+ const picked = this.picked.length;
687
+ const all = picked === rows.length;
688
+ const toggleAll = (on) => (this.picked = on ? rows.map(row => row.id) : []);
689
+ const label = picked ? t('picked', '{count} picked', { count: picked }) : t('pick-all', 'Pick all');
690
+ if (this.view === 'deleted') {
691
+ return html `<div class="pickbar">
692
+ <label><input type="checkbox" .checked=${all} .indeterminate=${picked > 0 && !all} @change=${(e) => toggleAll(e.target.checked)} />${label}</label>
693
+ ${picked
694
+ ? html `<button class="act" ?disabled=${this.busy} @click=${() => this.restoreMany(this.picked)}>${t('restore-many', 'Bring back {count}', { count: picked })}</button>
695
+ <button class="act unpick" @click=${() => (this.picked = [])}>${t('unpick', 'Clear the pick')}</button>`
696
+ : nothing}
697
+ </div>`;
698
+ }
699
+ const can = bulkActions(this.entries.filter(entry => this.picked.includes(entry.id)));
700
+ return html `<div class="pickbar">
701
+ <label><input type="checkbox" .checked=${all} .indeterminate=${picked > 0 && !all} @change=${(e) => toggleAll(e.target.checked)} />${label}</label>
702
+ ${picked
703
+ ? html `${can.remove.length
704
+ ? html `<button class="act danger" ?disabled=${this.busy} @click=${() => this.askRemoveMany(can.remove)}>${t('remove-many', 'Delete {count}', { count: can.remove.length })}</button>`
705
+ : nothing}
706
+ ${can.leave.length
707
+ ? html `<button class="act" ?disabled=${this.busy} @click=${() => this.askLeaveMany(can.leave)}>${t('leave-many', 'Leave {count}', { count: can.leave.length })}</button>`
708
+ : nothing}
709
+ ${can.hide.length
710
+ ? html `<button class="act" ?disabled=${this.busy} @click=${() => this.hideMany(can.hide)}>${t('hide-many', 'Hide {count}', { count: can.hide.length })}</button>`
711
+ : nothing}
712
+ <button class="act unpick" @click=${() => (this.picked = [])}>${t('unpick', 'Clear the pick')}</button>`
713
+ : nothing}
714
+ </div>`;
715
+ }
716
+ renderDeletedRow(one) {
717
+ const name = one.name || t('untitled', 'Untitled conversation');
718
+ const day = (at) => (at && Number.isFinite(Date.parse(at)) ? new Intl.DateTimeFormat(i18next.language || 'en', { month: 'short', day: 'numeric' }).format(new Date(at)) : '');
719
+ return html `<div class="row deleted">
720
+ <label class="pick-area"><input type="checkbox" class="pick" aria-label=${t('pick', 'Pick {name}', { name })} .checked=${this.picked.includes(one.id)}
721
+ @change=${(e) => this.togglePick(one.id, e.target.checked)} /></label>
722
+ <div class="open-row">
723
+ <div class="title">${name}</div>
724
+ <span class="meta">${t('deleted-on', 'Deleted {date}', { date: day(one.deletedAt) })}${one.purgesAt ? html ` · ${t('purges-on', 'emptied {date}', { date: day(one.purgesAt) })}` : nothing}</span>
725
+ </div>
726
+ <button class="restore" ?disabled=${this.busy} @click=${() => this.restoreOne(one)}>${t('restore', 'Bring back')}</button>
727
+ </div>`;
728
+ }
729
+ /** Why a row is where it is: archived, or suggested for clearing. */
730
+ renderState(entry) {
731
+ if (entry.cleanupReason === 'empty')
732
+ return html ` · ${t('reason-empty', 'Never asked anything')}`;
733
+ if (entry.cleanupReason === 'inactive')
734
+ return html ` · ${t('reason-inactive', 'No activity for {days} days', { days: entry.idleDays ?? '' })}`;
735
+ return entry.archived ? html ` · ${t('archived', 'Archived')}` : nothing;
736
+ }
503
737
  renderRow(entry) {
504
738
  const name = entry.name || t('untitled', 'Untitled conversation');
505
739
  const actions = sessionActions(entry);
@@ -526,11 +760,13 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
526
760
  return html `<div
527
761
  class="row ${entry.unread ? 'unread' : ''} ${entry.hidden ? 'hidden' : ''} ${entry.id === this.sessionId ? 'current' : ''} ${isOpen ? 'open-audience' : ''}"
528
762
  >
763
+ <label class="pick-area"><input type="checkbox" class="pick" aria-label=${t('pick', 'Pick {name}', { name })} .checked=${this.picked.includes(entry.id)}
764
+ @change=${(e) => this.togglePick(entry.id, e.target.checked)} /></label>
529
765
  <span class="dot"></span>
530
766
  <button class="open-row" aria-current=${entry.id === this.sessionId ? 'true' : 'false'} @click=${() => this.emit('assistant-session-select', { sessionId: entry.id, kind: entry.kind })}>
531
767
  <div class="title">${name}</div>
532
768
  <div class="prev">${entry.lastMessagePreview || ''}</div>
533
- <span class="meta">${this.renderAudience(entry.visibility)}${entry.lastMessageAt && Number.isFinite(Date.parse(entry.lastMessageAt)) ? html ` · <time datetime=${entry.lastMessageAt}>${new Intl.DateTimeFormat(i18next.language || 'en', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(entry.lastMessageAt))}</time>` : nothing}${entry.mine ? html ` · ${t('filter-mine', 'Started by me')}` : nothing}</span>
769
+ <span class="meta">${this.renderAudience(entry.visibility)}${entry.lastMessageAt && Number.isFinite(Date.parse(entry.lastMessageAt)) ? html ` · <time datetime=${entry.lastMessageAt}>${new Intl.DateTimeFormat(i18next.language || 'en', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(entry.lastMessageAt))}</time>` : nothing}${entry.mine ? html ` · ${t('filter-mine', 'Started by me')}` : nothing}${this.renderState(entry)}</span>
534
770
  </button>
535
771
  <button
536
772
  class="more-menu"
@@ -582,7 +818,7 @@ let AssistantSessionManager = class AssistantSessionManager extends LitElement {
582
818
  }));
583
819
  }
584
820
  async askRemove(entry, name) {
585
- const ok = await this.ask(t('remove', 'Delete'), t('confirm-remove', 'Delete "{name}"? It disappears for every participant and cannot be brought back.', { name }), t('remove', 'Delete'));
821
+ const ok = await this.ask(t('remove', 'Delete'), `${t('confirm-remove', 'Delete "{name}"? It disappears for every participant.', { name })} ${this.keptFor()}`, t('remove', 'Delete'));
586
822
  if (ok)
587
823
  this._removeSession(entry);
588
824
  }
@@ -684,6 +920,38 @@ __decorate([
684
920
  state(),
685
921
  __metadata("design:type", Object)
686
922
  ], AssistantSessionManager.prototype, "renamingId", void 0);
923
+ __decorate([
924
+ state(),
925
+ __metadata("design:type", String)
926
+ ], AssistantSessionManager.prototype, "view", void 0);
927
+ __decorate([
928
+ state(),
929
+ __metadata("design:type", Object)
930
+ ], AssistantSessionManager.prototype, "archivedCount", void 0);
931
+ __decorate([
932
+ state(),
933
+ __metadata("design:type", Object)
934
+ ], AssistantSessionManager.prototype, "cleanupCount", void 0);
935
+ __decorate([
936
+ state(),
937
+ __metadata("design:type", Object)
938
+ ], AssistantSessionManager.prototype, "keeping", void 0);
939
+ __decorate([
940
+ state(),
941
+ __metadata("design:type", Array)
942
+ ], AssistantSessionManager.prototype, "deleted", void 0);
943
+ __decorate([
944
+ state(),
945
+ __metadata("design:type", Array)
946
+ ], AssistantSessionManager.prototype, "picked", void 0);
947
+ __decorate([
948
+ state(),
949
+ __metadata("design:type", Object)
950
+ ], AssistantSessionManager.prototype, "busy", void 0);
951
+ __decorate([
952
+ state(),
953
+ __metadata("design:type", Object)
954
+ ], AssistantSessionManager.prototype, "notice", void 0);
687
955
  AssistantSessionManager = __decorate([
688
956
  customElement('ox-assistant-session-manager')
689
957
  ], AssistantSessionManager);