@things-factory/ai-assistant 10.1.214 → 10.1.218
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/client/components/assistant-dock.ts +6 -3
- package/client/components/assistant-session-manager.ts +263 -12
- package/client/utils/assistant-session-actions.ts +12 -0
- package/client/utils/assistant-session-admin.ts +70 -5
- package/dist-client/components/assistant-dock.js +6 -3
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/assistant-session-manager.d.ts +37 -0
- package/dist-client/components/assistant-session-manager.js +281 -13
- package/dist-client/components/assistant-session-manager.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/assistant-session-actions.d.ts +15 -0
- package/dist-client/utils/assistant-session-actions.js +11 -0
- package/dist-client/utils/assistant-session-actions.js.map +1 -1
- package/dist-client/utils/assistant-session-admin.d.ts +32 -1
- package/dist-client/utils/assistant-session-admin.js +36 -5
- package/dist-client/utils/assistant-session-admin.js.map +1 -1
- package/dist-server/service/assistant-chat-resolver.js +1 -1
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/index.d.ts +1 -1
- package/dist-server/service/assistant-mode/index.js +2 -1
- package/dist-server/service/assistant-mode/index.js.map +1 -1
- package/dist-server/service/assistant-mode/mode-categories.d.ts +15 -0
- package/dist-server/service/assistant-mode/mode-categories.js +7 -1
- package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
- package/dist-server/service/chat-session/session-cleanup.d.ts +28 -0
- package/dist-server/service/chat-session/session-cleanup.js +69 -0
- package/dist-server/service/chat-session/session-cleanup.js.map +1 -0
- package/dist-server/service/chat-session/session-inbox.d.ts +15 -0
- package/dist-server/service/chat-session/session-inbox.js +11 -0
- package/dist-server/service/chat-session/session-inbox.js.map +1 -1
- package/dist-server/service/chat-session/session-purge.d.ts +4 -0
- package/dist-server/service/chat-session/session-purge.js +45 -11
- package/dist-server/service/chat-session/session-purge.js.map +1 -1
- package/dist-server/service/chat-session-resolver.d.ts +24 -2
- package/dist-server/service/chat-session-resolver.js +193 -13
- package/dist-server/service/chat-session-resolver.js.map +1 -1
- package/dist-server/service/data-read/list-read.d.ts +3 -1
- package/dist-server/service/data-read/list-read.js +27 -2
- package/dist-server/service/data-read/list-read.js.map +1 -1
- package/dist-server/service/data-read/read-target.d.ts +16 -0
- package/dist-server/service/data-read/read-target.js +18 -1
- package/dist-server/service/data-read/read-target.js.map +1 -1
- package/dist-server/service/data-read/read-tools.js +3 -1
- package/dist-server/service/data-read/read-tools.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/server/service/assistant-chat-resolver.ts +2 -2
- package/server/service/assistant-mode/conversation-category.test.ts +15 -2
- package/server/service/assistant-mode/index.ts +1 -1
- package/server/service/assistant-mode/mode-categories.ts +17 -1
- package/server/service/assistant-mode/open-subject-door.test.ts +3 -1
- package/server/service/chat-session/session-cleanup.test.ts +83 -0
- package/server/service/chat-session/session-cleanup.ts +69 -0
- package/server/service/chat-session/session-inbox.ts +13 -0
- package/server/service/chat-session/session-purge.ts +46 -10
- package/server/service/chat-session-resolver.ts +150 -14
- package/server/service/data-read/list-read.ts +30 -3
- package/server/service/data-read/list-status.test.ts +46 -0
- package/server/service/data-read/read-target.ts +32 -1
- package/server/service/data-read/read-tools.ts +2 -1
- package/test/assistant-dock.test.ts +12 -1
- package/test/assistant-session-manager.test.ts +53 -1
- package/test/open-doors.test.ts +3 -1
- package/translations/en.json +37 -1
- package/translations/es.json +37 -1
- package/translations/ja.json +37 -1
- package/translations/ko.json +37 -1
- package/translations/ms.json +37 -1
- package/translations/zh.json +37 -1
|
@@ -521,14 +521,17 @@ export class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
521
521
|
if (!userId) throw new Error(t('no-user', 'your sign-in could not be read. sign in again.'))
|
|
522
522
|
|
|
523
523
|
await this.controller.open(this.target(userId, anchor))
|
|
524
|
-
await this.takeAsk()
|
|
525
524
|
} catch (error) {
|
|
526
525
|
if (ticket === this.revision) this.failure = (error as Error).message
|
|
527
526
|
} finally {
|
|
528
527
|
if (ticket === this.revision) {
|
|
529
528
|
this.loading = false
|
|
530
|
-
/*
|
|
531
|
-
|
|
529
|
+
/*
|
|
530
|
+
* The ask waiting is taken once the opening is done - takeAsk passes while loading, so taking it inside the
|
|
531
|
+
* opening left it waiting for good: a card's 「AI로 고치기」 reopened this screen's conversation and was never sent
|
|
532
|
+
* (2026-10-25). With no conversation possible here, it is answered now, not sent later somewhere else.
|
|
533
|
+
*/
|
|
534
|
+
if (this.anchor) void this.takeAsk()
|
|
532
535
|
/* What was being written on this subject before the page moved away comes back here. */
|
|
533
536
|
void this.restoreDraft()
|
|
534
537
|
}
|
|
@@ -10,15 +10,24 @@ import { OxPopupMenu } from '@operato/popup/ox-popup-menu.js'
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
assistantSessionAdmin,
|
|
13
|
+
bulkActions,
|
|
13
14
|
sessionActions,
|
|
14
15
|
type AssistantSessionAdmin,
|
|
15
16
|
type AssistantSessionEntry,
|
|
16
17
|
type AssistantSessionFilters,
|
|
18
|
+
type DeletedSessionEntry,
|
|
19
|
+
type SessionKeeping,
|
|
17
20
|
type SessionVisibility
|
|
18
21
|
} from '../utils/assistant-session-admin.js'
|
|
19
22
|
|
|
20
23
|
const PAGE = 30
|
|
21
24
|
|
|
25
|
+
/**
|
|
26
|
+
* What the list shows (shared-dock.md §2-7): the live ones, the live and the archived, the ones of mine it suggests
|
|
27
|
+
* clearing (picked already, so one press clears them), or the ones I deleted that can still come back.
|
|
28
|
+
*/
|
|
29
|
+
type SessionView = 'list' | 'archived' | 'cleanup' | 'deleted'
|
|
30
|
+
|
|
22
31
|
|
|
23
32
|
const t = (key: string, defaultValue: string, options: Record<string, unknown> = {}) =>
|
|
24
33
|
i18next.t(`ai-assistant.session.${key}`, { defaultValue, ...options })
|
|
@@ -46,6 +55,10 @@ const t = (key: string, defaultValue: string, options: Record<string, unknown> =
|
|
|
46
55
|
* ── Asking before what cannot be undone ─────────────────────────────────────────
|
|
47
56
|
* Delete, and opening a private conversation to the domain, ask on the row itself - [Cancel][OK].
|
|
48
57
|
* Making it private, hiding and renaming do not: each can be undone from the same row.
|
|
58
|
+
*
|
|
59
|
+
* ── Many at once (2026-10-25, the user: 「멀티셀렉트로 한번에 지울 수 있게」) ──────────
|
|
60
|
+
* Each row has a box; the picked ones are deleted, left or hidden together, each by its own row's hints
|
|
61
|
+
* (`bulkActions`), with one question. A deleted one is kept for `deletedDays` and comes back from 「Deleted」.
|
|
49
62
|
*/
|
|
50
63
|
@customElement('ox-assistant-session-manager')
|
|
51
64
|
export class AssistantSessionManager extends LitElement {
|
|
@@ -295,6 +308,38 @@ export class AssistantSessionManager extends LitElement {
|
|
|
295
308
|
.hiddenfilter md-icon {
|
|
296
309
|
--md-icon-size: 16px;
|
|
297
310
|
}
|
|
311
|
+
|
|
312
|
+
.pick { margin: 0; cursor: pointer; }
|
|
313
|
+
/* The box is small; the space around it picks too - a press beside it must not open the conversation. */
|
|
314
|
+
.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; }
|
|
315
|
+
.pickbar {
|
|
316
|
+
display: flex;
|
|
317
|
+
flex-wrap: wrap;
|
|
318
|
+
align-items: center;
|
|
319
|
+
gap: 4px 10px;
|
|
320
|
+
padding: 6px 10px;
|
|
321
|
+
border-bottom: 1px solid var(--md-sys-color-outline-variant);
|
|
322
|
+
font-size: 11px;
|
|
323
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
324
|
+
}
|
|
325
|
+
.pickbar label { display: inline-flex; align-items: center; gap: 4px; cursor: pointer; }
|
|
326
|
+
.pickbar .act,
|
|
327
|
+
.suggest,
|
|
328
|
+
.note .open-link,
|
|
329
|
+
.restore {
|
|
330
|
+
border: 0;
|
|
331
|
+
background: none;
|
|
332
|
+
padding: 0;
|
|
333
|
+
color: var(--md-sys-color-primary);
|
|
334
|
+
font: inherit;
|
|
335
|
+
cursor: pointer;
|
|
336
|
+
}
|
|
337
|
+
.pickbar .act:disabled { color: var(--md-sys-color-on-surface-variant); cursor: default; }
|
|
338
|
+
.pickbar .act.danger { color: var(--md-sys-color-error); }
|
|
339
|
+
.pickbar .unpick { margin-left: auto; }
|
|
340
|
+
.suggest { display: block; width: 100%; padding: 8px 10px; text-align: start; font-size: 12px; border-bottom: 1px solid var(--md-sys-color-outline-variant); }
|
|
341
|
+
.restore { flex: none; font-size: 12px; }
|
|
342
|
+
.row.deleted { cursor: default; }
|
|
298
343
|
`
|
|
299
344
|
|
|
300
345
|
/** The anchor whose conversations are listed - the same one the dock's session controller opened. */
|
|
@@ -319,6 +364,18 @@ export class AssistantSessionManager extends LitElement {
|
|
|
319
364
|
@state() private loading = false
|
|
320
365
|
@state() private error = ''
|
|
321
366
|
@state() private renamingId = ''
|
|
367
|
+
@state() private view: SessionView = 'list'
|
|
368
|
+
@state() private archivedCount = 0
|
|
369
|
+
@state() private cleanupCount = 0
|
|
370
|
+
@state() private keeping: SessionKeeping = { deletedDays: null, archiveDays: null }
|
|
371
|
+
@state() private deleted: DeletedSessionEntry[] = []
|
|
372
|
+
/** The rows picked for doing at once - ids, kept across a reload for the rows still listed. */
|
|
373
|
+
@state() private picked: string[] = []
|
|
374
|
+
@state() private busy = false
|
|
375
|
+
/** What was just done, said once - and the conversation brought back, to open it from here. */
|
|
376
|
+
@state() private notice: { text: string; open?: { sessionId: string; kind?: string } } | null = null
|
|
377
|
+
/** The suggestions arrive picked - set when the view turns to them, spent by the first read. */
|
|
378
|
+
private pickAllOnRead = false
|
|
322
379
|
|
|
323
380
|
/*
|
|
324
381
|
* The search and the filters stand open together, always (2026-10-12) - a filter behind a "Filters" button is one
|
|
@@ -326,7 +383,7 @@ export class AssistantSessionManager extends LitElement {
|
|
|
326
383
|
* to choose.
|
|
327
384
|
*/
|
|
328
385
|
private get filtering() {
|
|
329
|
-
return !!this.search.trim() || this.whose !== 'all' || this.visibility !== 'all' || this.includeHidden || this.unreadOnly
|
|
386
|
+
return !!this.search.trim() || this.whose !== 'all' || this.visibility !== 'all' || this.includeHidden || this.unreadOnly || this.view !== 'list'
|
|
330
387
|
}
|
|
331
388
|
|
|
332
389
|
private clearFilters() {
|
|
@@ -336,6 +393,7 @@ export class AssistantSessionManager extends LitElement {
|
|
|
336
393
|
this.whose = 'all'
|
|
337
394
|
this.visibility = 'all'
|
|
338
395
|
this.includeHidden = this.unreadOnly = false
|
|
396
|
+
this.view = 'list'
|
|
339
397
|
})
|
|
340
398
|
}
|
|
341
399
|
|
|
@@ -354,6 +412,17 @@ export class AssistantSessionManager extends LitElement {
|
|
|
354
412
|
this.loading = true
|
|
355
413
|
this.error = ''
|
|
356
414
|
try {
|
|
415
|
+
if (this.view === 'deleted') {
|
|
416
|
+
/* The deleted ones are few (kept for deletedDays) - the search narrows them by name here. */
|
|
417
|
+
const word = this.search.trim().toLowerCase()
|
|
418
|
+
const deleted = (await this.admin.deleted(this.anchorType, this.anchorId)).filter(one => !word || (one.name ?? '').toLowerCase().includes(word))
|
|
419
|
+
if (generation !== this.generation) return
|
|
420
|
+
this.deleted = deleted
|
|
421
|
+
this.entries = []
|
|
422
|
+
this.total = deleted.length
|
|
423
|
+
this.keepPicked(deleted.map(one => one.id))
|
|
424
|
+
return
|
|
425
|
+
}
|
|
357
426
|
/*
|
|
358
427
|
* Paging reads again from the start rather than appending - live activity reorders the list, and
|
|
359
428
|
* appending a later page to an earlier order duplicates and drops rows.
|
|
@@ -367,7 +436,9 @@ export class AssistantSessionManager extends LitElement {
|
|
|
367
436
|
whose: this.whose,
|
|
368
437
|
visibility: this.visibility,
|
|
369
438
|
includeHidden: this.includeHidden,
|
|
370
|
-
unreadOnly: this.unreadOnly
|
|
439
|
+
unreadOnly: this.unreadOnly,
|
|
440
|
+
includeArchived: this.view === 'archived',
|
|
441
|
+
cleanupOnly: this.view === 'cleanup'
|
|
371
442
|
},
|
|
372
443
|
limit: this.pageSize,
|
|
373
444
|
offset: 0
|
|
@@ -376,6 +447,10 @@ export class AssistantSessionManager extends LitElement {
|
|
|
376
447
|
this.entries = page.entries
|
|
377
448
|
this.total = page.total
|
|
378
449
|
this.hiddenCount = page.hiddenCount
|
|
450
|
+
this.archivedCount = page.archivedCount
|
|
451
|
+
this.cleanupCount = page.cleanupCount
|
|
452
|
+
this.keeping = page.keeping
|
|
453
|
+
this.keepPicked(page.entries.map(entry => entry.id))
|
|
379
454
|
} catch (error) {
|
|
380
455
|
if (generation !== this.generation) return
|
|
381
456
|
this.entries = []
|
|
@@ -386,6 +461,27 @@ export class AssistantSessionManager extends LitElement {
|
|
|
386
461
|
}
|
|
387
462
|
}
|
|
388
463
|
|
|
464
|
+
/** After a read: the suggestions arrive all picked; otherwise only the picked rows still listed stay picked. */
|
|
465
|
+
private keepPicked(listed: string[]) {
|
|
466
|
+
if (this.pickAllOnRead) {
|
|
467
|
+
this.pickAllOnRead = false
|
|
468
|
+
this.picked = listed
|
|
469
|
+
} else this.picked = this.picked.filter(id => listed.includes(id))
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private setView(view: SessionView) {
|
|
473
|
+
this.refilter(() => {
|
|
474
|
+
this.view = view
|
|
475
|
+
this.picked = []
|
|
476
|
+
this.notice = null
|
|
477
|
+
this.pickAllOnRead = view === 'cleanup'
|
|
478
|
+
})
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private togglePick(id: string, on: boolean) {
|
|
482
|
+
this.picked = on ? [...new Set([...this.picked, id])] : this.picked.filter(one => one !== id)
|
|
483
|
+
}
|
|
484
|
+
|
|
389
485
|
protected updated(changed: PropertyValues) {
|
|
390
486
|
if (changed.has('anchorType') || changed.has('anchorId') || changed.has('admin') || changed.has('kind')) void this.reload()
|
|
391
487
|
|
|
@@ -437,6 +533,7 @@ export class AssistantSessionManager extends LitElement {
|
|
|
437
533
|
/** Runs one door, then reads the list again - on success and on failure alike, so the row shows the truth. */
|
|
438
534
|
private async act(entry: AssistantSessionEntry, change: string, run: () => Promise<boolean>, refused: string) {
|
|
439
535
|
this.renamingId = ''
|
|
536
|
+
this.notice = null
|
|
440
537
|
try {
|
|
441
538
|
if (!(await run())) this.error = refused
|
|
442
539
|
else this.emit('assistant-session-changed', { sessionId: entry.id, change })
|
|
@@ -473,6 +570,75 @@ export class AssistantSessionManager extends LitElement {
|
|
|
473
570
|
void this.act(entry, 'removed', () => this.admin.remove(entry.id), t('remove-refused', 'this conversation could not be deleted.'))
|
|
474
571
|
}
|
|
475
572
|
|
|
573
|
+
/**
|
|
574
|
+
* Runs one door for each picked row, then reads the list again. Every success is told to the host like a row's own
|
|
575
|
+
* change, so it moves off an open conversation that went; every refusal is said by name.
|
|
576
|
+
*/
|
|
577
|
+
private async runMany(ids: string[], change: string, run: (id: string) => Promise<boolean>, done: (count: number) => string) {
|
|
578
|
+
if (!ids.length || this.busy) return
|
|
579
|
+
this.busy = true
|
|
580
|
+
this.error = ''
|
|
581
|
+
this.notice = null
|
|
582
|
+
const names = new Map([...this.entries.map(entry => [entry.id, entry.name] as const), ...this.deleted.map(one => [one.id, one.name] as const)])
|
|
583
|
+
const refused: string[] = []
|
|
584
|
+
let count = 0
|
|
585
|
+
for (const id of ids) {
|
|
586
|
+
try {
|
|
587
|
+
if (await run(id)) {
|
|
588
|
+
count++
|
|
589
|
+
this.emit('assistant-session-changed', { sessionId: id, change })
|
|
590
|
+
} else refused.push(names.get(id) || t('untitled', 'Untitled conversation'))
|
|
591
|
+
} catch (error) {
|
|
592
|
+
refused.push(`${names.get(id) || t('untitled', 'Untitled conversation')} (${(error as Error).message})`)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
this.picked = []
|
|
596
|
+
this.busy = false
|
|
597
|
+
const error = refused.length ? t('some-refused', 'Not done for {count}: {names}', { count: refused.length, names: refused.join(', ') }) : ''
|
|
598
|
+
await this.reload()
|
|
599
|
+
if (count) this.notice = { text: done(count) }
|
|
600
|
+
if (error) this.error = error
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** The sentence that says a deleted conversation can still come back - from 「Deleted」, for as long as it is kept. */
|
|
604
|
+
private keptFor(): string {
|
|
605
|
+
return this.keeping.deletedDays
|
|
606
|
+
? t('kept-for', 'You can bring it back from Deleted for {days} days.', { days: this.keeping.deletedDays })
|
|
607
|
+
: t('kept', 'You can bring it back from Deleted.')
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private async askRemoveMany(ids: string[]) {
|
|
611
|
+
const ok = await this.ask(
|
|
612
|
+
t('remove', 'Delete'),
|
|
613
|
+
`${t('confirm-remove-many', 'Delete {count} conversations? They disappear for every participant.', { count: ids.length })} ${this.keptFor()}`,
|
|
614
|
+
t('remove', 'Delete')
|
|
615
|
+
)
|
|
616
|
+
if (ok) void this.runMany(ids, 'removed', id => this.admin.remove(id), count => t('removed-many', '{count} deleted.', { count }))
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private async askLeaveMany(ids: string[]) {
|
|
620
|
+
const ok = await this.ask(
|
|
621
|
+
t('leave', 'Leave'),
|
|
622
|
+
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 }),
|
|
623
|
+
t('leave', 'Leave')
|
|
624
|
+
)
|
|
625
|
+
if (ok) void this.runMany(ids, 'left', id => this.admin.leave(id), count => t('left-many', 'Left {count}.', { count }))
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
private hideMany(ids: string[]) {
|
|
629
|
+
void this.runMany(ids, 'hidden', id => this.admin.hide(id, true), count => t('hidden-many', '{count} hidden from your list.', { count }))
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private restoreMany(ids: string[]) {
|
|
633
|
+
void this.runMany(ids, 'restored', id => this.admin.restore(id), count => t('restored-many', '{count} brought back.', { count }))
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
private async restoreOne(one: DeletedSessionEntry) {
|
|
637
|
+
const name = one.name || t('untitled', 'Untitled conversation')
|
|
638
|
+
await this.runMany([one.id], 'restored', id => this.admin.restore(id), () => t('restored', '"{name}" is back.', { name }))
|
|
639
|
+
if (this.notice) this.notice = { ...this.notice, open: { sessionId: one.id, kind: one.kind } }
|
|
640
|
+
}
|
|
641
|
+
|
|
476
642
|
/* ── drawing ─────────────────────────────────────────────────────────────────────── */
|
|
477
643
|
|
|
478
644
|
render(): TemplateResult {
|
|
@@ -490,7 +656,7 @@ export class AssistantSessionManager extends LitElement {
|
|
|
490
656
|
<button class="new" @click=${() => this.emit('assistant-session-reset')}>+ ${t('new', 'New conversation')}</button>
|
|
491
657
|
</div>
|
|
492
658
|
<div class="filters">
|
|
493
|
-
|
|
659
|
+
${this.view === 'deleted' ? nothing : html`<select aria-label=${t('filter-whose', 'Whose')} .value=${this.whose}
|
|
494
660
|
@change=${(e: Event) => this.refilter(() => (this.whose = (e.target as HTMLSelectElement).value as any))}>
|
|
495
661
|
<option value="all">${t('filter-all', 'All')}</option>
|
|
496
662
|
<option value="mine">${t('filter-mine', 'Started by me')}</option>
|
|
@@ -502,32 +668,115 @@ export class AssistantSessionManager extends LitElement {
|
|
|
502
668
|
<option value="open">${t('filter-open', 'Public')}</option>
|
|
503
669
|
<option value="group">${t('filter-group', 'Group')}</option>
|
|
504
670
|
<option value="private">${t('filter-private', 'Private')}</option>
|
|
671
|
+
</select>`}
|
|
672
|
+
<select aria-label=${t('filter-view', 'Show')} .value=${this.view}
|
|
673
|
+
@change=${(e: Event) => this.setView((e.target as HTMLSelectElement).value as SessionView)}>
|
|
674
|
+
<option value="list">${t('view-list', 'Active')}</option>
|
|
675
|
+
<option value="archived">${t('view-archived', 'Including archived')}</option>
|
|
676
|
+
<option value="cleanup">${t('view-cleanup', 'Suggested to clear')}</option>
|
|
677
|
+
<option value="deleted">${t('view-deleted', 'Deleted')}</option>
|
|
505
678
|
</select>
|
|
506
|
-
|
|
679
|
+
${this.view === 'deleted' ? nothing : html`<label><input type="checkbox" .checked=${this.unreadOnly}
|
|
507
680
|
@change=${(e: Event) => this.refilter(() => (this.unreadOnly = (e.target as HTMLInputElement).checked))} />${t('filter-unread', 'Unread')}</label>
|
|
508
681
|
<label><input type="checkbox" .checked=${this.includeHidden}
|
|
509
|
-
@change=${(e: Event) => this.refilter(() => (this.includeHidden = (e.target as HTMLInputElement).checked))} />${t('filter-hidden', 'Including hidden')}</label
|
|
682
|
+
@change=${(e: Event) => this.refilter(() => (this.includeHidden = (e.target as HTMLInputElement).checked))} />${t('filter-hidden', 'Including hidden')}</label>`}
|
|
510
683
|
${this.filtering ? html`<button class="clear" @click=${() => this.clearFilters()}>${t('reset-filters', 'Clear search and filters')}</button>` : nothing}
|
|
511
684
|
</div>
|
|
685
|
+
${this.renderPickBar()}
|
|
512
686
|
<div class="list">
|
|
513
687
|
${this.error ? html`<div class="note" role="alert">${this.error}</div>` : nothing}
|
|
688
|
+
${this.notice
|
|
689
|
+
? html`<div class="note" role="status">${this.notice.text}${this.notice.open
|
|
690
|
+
? html` <button class="open-link" @click=${() => this.emit('assistant-session-select', this.notice!.open)}>${t('open', 'Open')}</button>`
|
|
691
|
+
: nothing}</div>`
|
|
692
|
+
: nothing}
|
|
514
693
|
${this.loading ? html`<div class="note" role="status">${t('loading', 'Loading conversations…')}</div>` : nothing}
|
|
515
|
-
${
|
|
516
|
-
? html`<
|
|
517
|
-
:
|
|
518
|
-
${
|
|
694
|
+
${this.view === 'list' && this.cleanupCount > 0 && !this.search.trim()
|
|
695
|
+
? html`<button class="suggest" @click=${() => this.setView('cleanup')}>${t('cleanup-suggest', '{count} of yours look ready to clear - see them', { count: this.cleanupCount })}</button>`
|
|
696
|
+
: nothing}
|
|
697
|
+
${this.view === 'deleted'
|
|
698
|
+
? this.deleted.length === 0 && !this.loading && !this.error
|
|
699
|
+
? html`<div class="empty" role="status">${t('deleted-empty', 'Nothing you deleted here is kept.')}</div>`
|
|
700
|
+
: this.deleted.map(one => this.renderDeletedRow(one))
|
|
701
|
+
: shown === 0 && !this.loading && !this.error
|
|
702
|
+
? html`<div class="empty" role="status">${this.view === 'cleanup'
|
|
703
|
+
? t('cleanup-empty', 'Nothing of yours to clear here.')
|
|
704
|
+
: this.filtering ? t('filtered-empty', 'No conversations match these filters here.') : t('scope-empty', 'No conversations yet.')}</div>`
|
|
705
|
+
: this.entries.map(entry => this.renderRow(entry))}
|
|
706
|
+
${this.view !== 'deleted' && shown < this.total
|
|
519
707
|
? html`<button class="more" @click=${() => this.loadMore()}>${t('load-more', 'Show more ({rest})', { rest: this.total - shown })}</button>`
|
|
520
708
|
: nothing}
|
|
521
709
|
</div>
|
|
522
710
|
<!-- A hidden conversation must not be forgotten: say how many, and one press shows them. -->
|
|
523
|
-
${this.hiddenCount > 0 && !this.includeHidden
|
|
711
|
+
${this.hiddenCount > 0 && !this.includeHidden && (this.view === 'list' || this.view === 'archived')
|
|
524
712
|
? html`<button class="hiddenfilter" @click=${() => this.refilter(() => (this.includeHidden = true))}>
|
|
525
713
|
<md-icon>visibility_off</md-icon>${t('hidden-count', '{count} hidden', { count: this.hiddenCount })}
|
|
526
714
|
</button>`
|
|
527
715
|
: nothing}
|
|
716
|
+
${this.archivedCount > 0 && this.view === 'list'
|
|
717
|
+
? html`<button class="hiddenfilter" @click=${() => this.setView('archived')}>
|
|
718
|
+
<md-icon>inventory_2</md-icon>${t('archived-count', '{count} archived - no activity for {days} days', { count: this.archivedCount, days: this.keeping.archiveDays ?? '' })}
|
|
719
|
+
</button>`
|
|
720
|
+
: nothing}
|
|
528
721
|
`
|
|
529
722
|
}
|
|
530
723
|
|
|
724
|
+
/** The box over the list: pick all, and what the picked ones can have done to them together. */
|
|
725
|
+
private renderPickBar() {
|
|
726
|
+
const rows: { id: string }[] = this.view === 'deleted' ? this.deleted : this.entries
|
|
727
|
+
if (!rows.length) return nothing
|
|
728
|
+
const picked = this.picked.length
|
|
729
|
+
const all = picked === rows.length
|
|
730
|
+
const toggleAll = (on: boolean) => (this.picked = on ? rows.map(row => row.id) : [])
|
|
731
|
+
const label = picked ? t('picked', '{count} picked', { count: picked }) : t('pick-all', 'Pick all')
|
|
732
|
+
if (this.view === 'deleted') {
|
|
733
|
+
return html`<div class="pickbar">
|
|
734
|
+
<label><input type="checkbox" .checked=${all} .indeterminate=${picked > 0 && !all} @change=${(e: Event) => toggleAll((e.target as HTMLInputElement).checked)} />${label}</label>
|
|
735
|
+
${picked
|
|
736
|
+
? html`<button class="act" ?disabled=${this.busy} @click=${() => this.restoreMany(this.picked)}>${t('restore-many', 'Bring back {count}', { count: picked })}</button>
|
|
737
|
+
<button class="act unpick" @click=${() => (this.picked = [])}>${t('unpick', 'Clear the pick')}</button>`
|
|
738
|
+
: nothing}
|
|
739
|
+
</div>`
|
|
740
|
+
}
|
|
741
|
+
const can = bulkActions(this.entries.filter(entry => this.picked.includes(entry.id)))
|
|
742
|
+
return html`<div class="pickbar">
|
|
743
|
+
<label><input type="checkbox" .checked=${all} .indeterminate=${picked > 0 && !all} @change=${(e: Event) => toggleAll((e.target as HTMLInputElement).checked)} />${label}</label>
|
|
744
|
+
${picked
|
|
745
|
+
? html`${can.remove.length
|
|
746
|
+
? html`<button class="act danger" ?disabled=${this.busy} @click=${() => this.askRemoveMany(can.remove)}>${t('remove-many', 'Delete {count}', { count: can.remove.length })}</button>`
|
|
747
|
+
: nothing}
|
|
748
|
+
${can.leave.length
|
|
749
|
+
? html`<button class="act" ?disabled=${this.busy} @click=${() => this.askLeaveMany(can.leave)}>${t('leave-many', 'Leave {count}', { count: can.leave.length })}</button>`
|
|
750
|
+
: nothing}
|
|
751
|
+
${can.hide.length
|
|
752
|
+
? html`<button class="act" ?disabled=${this.busy} @click=${() => this.hideMany(can.hide)}>${t('hide-many', 'Hide {count}', { count: can.hide.length })}</button>`
|
|
753
|
+
: nothing}
|
|
754
|
+
<button class="act unpick" @click=${() => (this.picked = [])}>${t('unpick', 'Clear the pick')}</button>`
|
|
755
|
+
: nothing}
|
|
756
|
+
</div>`
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
private renderDeletedRow(one: DeletedSessionEntry): TemplateResult {
|
|
760
|
+
const name = one.name || t('untitled', 'Untitled conversation')
|
|
761
|
+
const day = (at?: string) => (at && Number.isFinite(Date.parse(at)) ? new Intl.DateTimeFormat(i18next.language || 'en', { month: 'short', day: 'numeric' }).format(new Date(at)) : '')
|
|
762
|
+
return html`<div class="row deleted">
|
|
763
|
+
<label class="pick-area"><input type="checkbox" class="pick" aria-label=${t('pick', 'Pick {name}', { name })} .checked=${this.picked.includes(one.id)}
|
|
764
|
+
@change=${(e: Event) => this.togglePick(one.id, (e.target as HTMLInputElement).checked)} /></label>
|
|
765
|
+
<div class="open-row">
|
|
766
|
+
<div class="title">${name}</div>
|
|
767
|
+
<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>
|
|
768
|
+
</div>
|
|
769
|
+
<button class="restore" ?disabled=${this.busy} @click=${() => this.restoreOne(one)}>${t('restore', 'Bring back')}</button>
|
|
770
|
+
</div>`
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Why a row is where it is: archived, or suggested for clearing. */
|
|
774
|
+
private renderState(entry: AssistantSessionEntry) {
|
|
775
|
+
if (entry.cleanupReason === 'empty') return html` · ${t('reason-empty', 'Never asked anything')}`
|
|
776
|
+
if (entry.cleanupReason === 'inactive') return html` · ${t('reason-inactive', 'No activity for {days} days', { days: entry.idleDays ?? '' })}`
|
|
777
|
+
return entry.archived ? html` · ${t('archived', 'Archived')}` : nothing
|
|
778
|
+
}
|
|
779
|
+
|
|
531
780
|
private renderRow(entry: AssistantSessionEntry): TemplateResult {
|
|
532
781
|
const name = entry.name || t('untitled', 'Untitled conversation')
|
|
533
782
|
const actions = sessionActions(entry)
|
|
@@ -552,11 +801,13 @@ export class AssistantSessionManager extends LitElement {
|
|
|
552
801
|
return html`<div
|
|
553
802
|
class="row ${entry.unread ? 'unread' : ''} ${entry.hidden ? 'hidden' : ''} ${entry.id === this.sessionId ? 'current' : ''} ${isOpen ? 'open-audience' : ''}"
|
|
554
803
|
>
|
|
804
|
+
<label class="pick-area"><input type="checkbox" class="pick" aria-label=${t('pick', 'Pick {name}', { name })} .checked=${this.picked.includes(entry.id)}
|
|
805
|
+
@change=${(e: Event) => this.togglePick(entry.id, (e.target as HTMLInputElement).checked)} /></label>
|
|
555
806
|
<span class="dot"></span>
|
|
556
807
|
<button class="open-row" aria-current=${entry.id === this.sessionId ? 'true' : 'false'} @click=${() => this.emit('assistant-session-select', { sessionId: entry.id, kind: entry.kind })}>
|
|
557
808
|
<div class="title">${name}</div>
|
|
558
809
|
<div class="prev">${entry.lastMessagePreview || ''}</div>
|
|
559
|
-
<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>
|
|
810
|
+
<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>
|
|
560
811
|
</button>
|
|
561
812
|
<button
|
|
562
813
|
class="more-menu"
|
|
@@ -613,7 +864,7 @@ export class AssistantSessionManager extends LitElement {
|
|
|
613
864
|
private async askRemove(entry: AssistantSessionEntry, name: string) {
|
|
614
865
|
const ok = await this.ask(
|
|
615
866
|
t('remove', 'Delete'),
|
|
616
|
-
t('confirm-remove', 'Delete "{name}"? It disappears for every participant
|
|
867
|
+
`${t('confirm-remove', 'Delete "{name}"? It disappears for every participant.', { name })} ${this.keptFor()}`,
|
|
617
868
|
t('remove', 'Delete')
|
|
618
869
|
)
|
|
619
870
|
if (ok) this._removeSession(entry)
|
|
@@ -18,3 +18,15 @@ export function sessionActions(entry: { participant?: boolean; hidden?: boolean;
|
|
|
18
18
|
leave: !!entry.participant && !entry.owner
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What a picked set of rows can have done to it at once - each row by the same hints as its own menu: delete the
|
|
24
|
+
* ones I started, leave the ones I am only in, hide any (2026-10-25, the user: 「멀티셀렉트로 한번에 지울 수 있게」).
|
|
25
|
+
*/
|
|
26
|
+
export function bulkActions(entries: { id: string; participant?: boolean; hidden?: boolean; mine?: boolean; owner?: boolean }[]) {
|
|
27
|
+
return {
|
|
28
|
+
remove: entries.filter(entry => sessionActions(entry).remove).map(entry => entry.id),
|
|
29
|
+
leave: entries.filter(entry => !sessionActions(entry).remove && sessionActions(entry).leave).map(entry => entry.id),
|
|
30
|
+
hide: entries.filter(entry => sessionActions(entry).hide).map(entry => entry.id)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -2,7 +2,7 @@ import { client } from '@operato/graphql'
|
|
|
2
2
|
import { i18next } from '@operato/i18n'
|
|
3
3
|
import gql from 'graphql-tag'
|
|
4
4
|
|
|
5
|
-
export { sessionActions } from './assistant-session-actions.js'
|
|
5
|
+
export { bulkActions, sessionActions } from './assistant-session-actions.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Managing the conversations on one anchor - list, rename, hide, delete, and who may read them.
|
|
@@ -18,6 +18,7 @@ export { sessionActions } from './assistant-session-actions.js'
|
|
|
18
18
|
* delete the creator (`mine`) (not reversible from a screen)
|
|
19
19
|
* visibility the owner (`owner`) (opening shows what is already written)
|
|
20
20
|
* leave a member, never the owner (in a group, leaving ends reading here)
|
|
21
|
+
* restore the creator, while it is kept (deleted ones are kept `deletedDays`, shared-dock.md §9-5)
|
|
21
22
|
*/
|
|
22
23
|
|
|
23
24
|
/** Who reads it: everyone in the domain, the people the owner brought in, or the owner alone (ADR-0073 decision 3). */
|
|
@@ -40,6 +41,27 @@ export interface AssistantSessionEntry {
|
|
|
40
41
|
/** Hidden from my own list (returned only with the hidden filter on). */
|
|
41
42
|
hidden?: boolean
|
|
42
43
|
visibility?: SessionVisibility | string
|
|
44
|
+
/** No activity for the archive period - out of the list until asked for or searched. */
|
|
45
|
+
archived?: boolean
|
|
46
|
+
/** Why the list suggests clearing it (mine only): never asked anything, or idle for 30 days. */
|
|
47
|
+
cleanupReason?: 'empty' | 'inactive' | string
|
|
48
|
+
/** Whole days since the last activity. */
|
|
49
|
+
idleDays?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A conversation I deleted that is still kept - it can be brought back until `purgesAt`. */
|
|
53
|
+
export interface DeletedSessionEntry {
|
|
54
|
+
id: string
|
|
55
|
+
name?: string
|
|
56
|
+
kind?: string
|
|
57
|
+
deletedAt: string
|
|
58
|
+
purgesAt?: string
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** How long conversations are kept; `null` when that keeping is off. */
|
|
62
|
+
export interface SessionKeeping {
|
|
63
|
+
deletedDays: number | null
|
|
64
|
+
archiveDays: number | null
|
|
43
65
|
}
|
|
44
66
|
|
|
45
67
|
/** Legacy single filter - kept for callers written before the filter set. */
|
|
@@ -53,6 +75,10 @@ export interface AssistantSessionFilters {
|
|
|
53
75
|
visibility?: 'all' | SessionVisibility
|
|
54
76
|
includeHidden?: boolean
|
|
55
77
|
unreadOnly?: boolean
|
|
78
|
+
/** Also the archived ones. */
|
|
79
|
+
includeArchived?: boolean
|
|
80
|
+
/** Only my own the list suggests clearing - hidden and archived ones included. */
|
|
81
|
+
cleanupOnly?: boolean
|
|
56
82
|
}
|
|
57
83
|
|
|
58
84
|
export interface AssistantSessionQuery {
|
|
@@ -71,6 +97,11 @@ export interface AssistantSessionPage {
|
|
|
71
97
|
total: number
|
|
72
98
|
/** How many I hid - so a hidden conversation is never forgotten. */
|
|
73
99
|
hiddenCount: number
|
|
100
|
+
/** How many are archived - the same, for the ones left idle. */
|
|
101
|
+
archivedCount: number
|
|
102
|
+
/** How many of mine the list suggests clearing. */
|
|
103
|
+
cleanupCount: number
|
|
104
|
+
keeping: SessionKeeping
|
|
74
105
|
}
|
|
75
106
|
|
|
76
107
|
export interface AssistantSessionAdmin {
|
|
@@ -80,6 +111,8 @@ export interface AssistantSessionAdmin {
|
|
|
80
111
|
remove(sessionId: string): Promise<boolean>
|
|
81
112
|
setVisibility(sessionId: string, visibility: SessionVisibility): Promise<boolean>
|
|
82
113
|
leave(sessionId: string): Promise<boolean>
|
|
114
|
+
deleted(anchorType: string, anchorId: string): Promise<DeletedSessionEntry[]>
|
|
115
|
+
restore(sessionId: string): Promise<boolean>
|
|
83
116
|
}
|
|
84
117
|
|
|
85
118
|
/** The page size the server allows at most (`chatSessionInbox` clamps to 200). */
|
|
@@ -98,10 +131,13 @@ export const assistantSessionAdmin: AssistantSessionAdmin = {
|
|
|
98
131
|
const mine = filters.whose ? filters.whose === 'mine' : filter === 'mine'
|
|
99
132
|
const result = await client.query({
|
|
100
133
|
query: gql`
|
|
101
|
-
query ($t: String!, $i: String!, $h: Boolean, $q: String, $m: Boolean, $l: Int, $o: Int, $k: String, $p: Boolean, $v: String, $u: Boolean) {
|
|
134
|
+
query ($t: String!, $i: String!, $h: Boolean, $q: String, $m: Boolean, $l: Int, $o: Int, $k: String, $p: Boolean, $v: String, $u: Boolean, $a: Boolean, $c: Boolean) {
|
|
102
135
|
hiddenSessionCount(anchorType: $t, anchorId: $i)
|
|
103
|
-
|
|
104
|
-
|
|
136
|
+
archivedSessionCount(anchorType: $t, anchorId: $i)
|
|
137
|
+
cleanupSessionCount(anchorType: $t, anchorId: $i)
|
|
138
|
+
aiSessionKeeping { deletedDays archiveDays }
|
|
139
|
+
chatSessionInboxTotal(anchorType: $t, anchorId: $i, showHidden: $h, search: $q, mineOnly: $m, kind: $k, participantOnly: $p, visibility: $v, unreadOnly: $u, includeArchived: $a, cleanupOnly: $c)
|
|
140
|
+
chatSessionInbox(anchorType: $t, anchorId: $i, showHidden: $h, search: $q, mineOnly: $m, kind: $k, participantOnly: $p, visibility: $v, unreadOnly: $u, includeArchived: $a, cleanupOnly: $c, limit: $l, offset: $o) {
|
|
105
141
|
id
|
|
106
142
|
name
|
|
107
143
|
kind
|
|
@@ -114,6 +150,9 @@ export const assistantSessionAdmin: AssistantSessionAdmin = {
|
|
|
114
150
|
owner
|
|
115
151
|
hidden
|
|
116
152
|
visibility
|
|
153
|
+
archived
|
|
154
|
+
cleanupReason
|
|
155
|
+
idleDays
|
|
117
156
|
}
|
|
118
157
|
}
|
|
119
158
|
`,
|
|
@@ -128,6 +167,8 @@ export const assistantSessionAdmin: AssistantSessionAdmin = {
|
|
|
128
167
|
p: filters.whose === 'participant' || null,
|
|
129
168
|
v: filters.visibility && filters.visibility !== 'all' ? filters.visibility : null,
|
|
130
169
|
u: filters.unreadOnly || null,
|
|
170
|
+
a: filters.includeArchived || null,
|
|
171
|
+
c: filters.cleanupOnly || null,
|
|
131
172
|
l: Math.min(SESSION_PAGE_MAX, Math.max(1, limit)),
|
|
132
173
|
o: Math.max(0, offset)
|
|
133
174
|
},
|
|
@@ -144,7 +185,13 @@ export const assistantSessionAdmin: AssistantSessionAdmin = {
|
|
|
144
185
|
/* With the hidden filter the server returns hidden ones alongside the rest, marked - the total counts both. */
|
|
145
186
|
entries,
|
|
146
187
|
total: result.data?.chatSessionInboxTotal ?? entries.length,
|
|
147
|
-
hiddenCount: result.data?.hiddenSessionCount ?? 0
|
|
188
|
+
hiddenCount: result.data?.hiddenSessionCount ?? 0,
|
|
189
|
+
archivedCount: result.data?.archivedSessionCount ?? 0,
|
|
190
|
+
cleanupCount: result.data?.cleanupSessionCount ?? 0,
|
|
191
|
+
keeping: {
|
|
192
|
+
deletedDays: result.data?.aiSessionKeeping?.deletedDays ?? null,
|
|
193
|
+
archiveDays: result.data?.aiSessionKeeping?.archiveDays ?? null
|
|
194
|
+
}
|
|
148
195
|
}
|
|
149
196
|
},
|
|
150
197
|
|
|
@@ -170,5 +217,23 @@ export const assistantSessionAdmin: AssistantSessionAdmin = {
|
|
|
170
217
|
|
|
171
218
|
leave(sessionId) {
|
|
172
219
|
return mutate(gql`mutation ($id: String!) { leaveAISession(sessionId: $id) }`, { id: sessionId }, 'leaveAISession')
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
async deleted(anchorType, anchorId) {
|
|
223
|
+
const result = await client.query({
|
|
224
|
+
query: gql`
|
|
225
|
+
query ($t: String!, $i: String!) {
|
|
226
|
+
deletedAISessions(anchorType: $t, anchorId: $i) { id name kind deletedAt purgesAt }
|
|
227
|
+
}
|
|
228
|
+
`,
|
|
229
|
+
variables: { t: anchorType, i: anchorId },
|
|
230
|
+
fetchPolicy: 'no-cache'
|
|
231
|
+
})
|
|
232
|
+
if (result.errors?.length) throw new Error(result.errors[0].message)
|
|
233
|
+
return result.data?.deletedAISessions ?? []
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
restore(sessionId) {
|
|
237
|
+
return mutate(gql`mutation ($id: String!) { restoreAISession(sessionId: $id) }`, { id: sessionId }, 'restoreAISession')
|
|
173
238
|
}
|
|
174
239
|
}
|
|
@@ -535,7 +535,6 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
535
535
|
if (!userId)
|
|
536
536
|
throw new Error(t('no-user', 'your sign-in could not be read. sign in again.'));
|
|
537
537
|
await this.controller.open(this.target(userId, anchor));
|
|
538
|
-
await this.takeAsk();
|
|
539
538
|
}
|
|
540
539
|
catch (error) {
|
|
541
540
|
if (ticket === this.revision)
|
|
@@ -544,8 +543,12 @@ let AssistantDock = class AssistantDock extends localize(i18next)(LitElement) {
|
|
|
544
543
|
finally {
|
|
545
544
|
if (ticket === this.revision) {
|
|
546
545
|
this.loading = false;
|
|
547
|
-
/*
|
|
548
|
-
|
|
546
|
+
/*
|
|
547
|
+
* The ask waiting is taken once the opening is done - takeAsk passes while loading, so taking it inside the
|
|
548
|
+
* opening left it waiting for good: a card's 「AI로 고치기」 reopened this screen's conversation and was never sent
|
|
549
|
+
* (2026-10-25). With no conversation possible here, it is answered now, not sent later somewhere else.
|
|
550
|
+
*/
|
|
551
|
+
if (this.anchor)
|
|
549
552
|
void this.takeAsk();
|
|
550
553
|
/* What was being written on this subject before the page moved away comes back here. */
|
|
551
554
|
void this.restoreDraft();
|