@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
@@ -1,3 +1,27 @@
1
+ /**
2
+ * The grid's own row picker - a list that hands its rows to the assistant shows it beside `ox-assistant-selection-bar`
3
+ * (data-grist gutter `row-selector`, many at once).
4
+ */
5
+ export declare const SELECTION_ROW_PICKER: {
6
+ type: string;
7
+ gutterName: string;
8
+ multiple: boolean;
9
+ };
10
+ /** A picked row as the person reads it, when the screen gives no words of its own - its code or number, then its name. */
11
+ export declare function selectionLabel(row: any): string;
12
+ /**
13
+ * The rows a person picked, as the assistant is handed them (`askAssistant({ selection })`): saved rows only (an id, no
14
+ * pending edit), each once, in screen order - never whole rows. Given the list's axis, each id is the record's key
15
+ * `<axis>:<uuid>`, the one that list's read tool takes (read-target.ts `recordIdOfKey`): a bare uuid names no tool, and
16
+ * a model read a contract's id with a dashboard's card tool (2026-10-25, Office 미수금).
17
+ */
18
+ export declare function selectedRecords(rows: any[], options?: {
19
+ axis?: string;
20
+ labelOf?: (row: any) => string;
21
+ }): {
22
+ id: string;
23
+ label: string;
24
+ }[];
1
25
  /** The line the model reads begins with this; the conversation does not draw it. */
2
26
  export declare const SELECTION_KEYS_PREFIX = "[keys]";
3
27
  /** Labels named on the person's line - the rest are counted. */
@@ -6,6 +6,30 @@
6
6
  * model is handed the note as stored. Until 2026-10-24 the screen sent the selection as JSON and the conversation
7
7
  * opened on `{"source":"explicit-page-selection",…}`.
8
8
  */
9
+ /**
10
+ * The grid's own row picker - a list that hands its rows to the assistant shows it beside `ox-assistant-selection-bar`
11
+ * (data-grist gutter `row-selector`, many at once).
12
+ */
13
+ export const SELECTION_ROW_PICKER = { type: 'gutter', gutterName: 'row-selector', multiple: true };
14
+ /** A picked row as the person reads it, when the screen gives no words of its own - its code or number, then its name. */
15
+ export function selectionLabel(row) {
16
+ return [row?.code || row?.number, row?.name || row?.title || row?.description]
17
+ .filter(Boolean)
18
+ .join(' · ');
19
+ }
20
+ /**
21
+ * The rows a person picked, as the assistant is handed them (`askAssistant({ selection })`): saved rows only (an id, no
22
+ * pending edit), each once, in screen order - never whole rows. Given the list's axis, each id is the record's key
23
+ * `<axis>:<uuid>`, the one that list's read tool takes (read-target.ts `recordIdOfKey`): a bare uuid names no tool, and
24
+ * a model read a contract's id with a dashboard's card tool (2026-10-25, Office 미수금).
25
+ */
26
+ export function selectedRecords(rows, options = {}) {
27
+ const { axis, labelOf = selectionLabel } = options;
28
+ const seen = new Set();
29
+ return (rows ?? [])
30
+ .filter(row => row?.id && !row.__dirty__ && !seen.has(row.id) && !!seen.add(row.id))
31
+ .map(row => ({ id: axis ? `${axis}:${row.id}` : row.id, label: labelOf(row) || '' }));
32
+ }
9
33
  /** The line the model reads begins with this; the conversation does not draw it. */
10
34
  export const SELECTION_KEYS_PREFIX = '[keys]';
11
35
  /** Labels named on the person's line - the rest are counted. */
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-selection.js","sourceRoot":"","sources":["../../client/utils/assistant-selection.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,oFAAoF;AACpF,MAAM,CAAC,MAAM,qBAAqB,GAAG,QAAQ,CAAA;AAE7C,gEAAgE;AAChE,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAA;AAYvC,iHAAiH;AACjH,MAAM,UAAU,aAAa,CAAC,SAA6B,EAAE,CAAqB;IAChF,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,MAAM,OAAO,GAAG,CAAC,SAAS,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IAC5G,IAAI,CAAC,OAAO,CAAC,MAAM;QAAE,OAAO,EAAE,CAAA;IAC9B,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;IAChG,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAChE,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAAA;IAC5E,MAAM,IAAI,GAAG,CAAC,CAAC,gBAAgB,EAAE,4BAA4B,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAC,CAAA;IACjH,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,gBAAgB,EAAE,kBAAkB,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;IACjI,MAAM,IAAI,GAAG,GAAG,qBAAqB,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;IAC5H,OAAO,GAAG,KAAK,KAAK,IAAI,EAAE,CAAA;AAC5B,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;SACnB,KAAK,CAAC,IAAI,CAAC;SACX,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,qBAAqB,CAAC,CAAC;SACvD,IAAI,CAAC,IAAI,CAAC;SACV,IAAI,EAAE,CAAA;AACX,CAAC","sourcesContent":["/*\n * The records a person picked on a screen and handed to the assistant - `askAssistant({ selection })`.\n *\n * The conversation keeps them as a note: one line the person reads (「선택 1건 · 계약 대장: H2022019 · 브라이스톤 IT3.0\n * Website」) and one line the model reads - the list and the ids, after `[keys]`. The screen hides the key line; the\n * model is handed the note as stored. Until 2026-10-24 the screen sent the selection as JSON and the conversation\n * opened on `{\"source\":\"explicit-page-selection\",…}`.\n */\n\n/** The line the model reads begins with this; the conversation does not draw it. */\nexport const SELECTION_KEYS_PREFIX = '[keys]'\n\n/** Labels named on the person's line - the rest are counted. */\nexport const SELECTION_LABELS_SHOWN = 5\n\nexport interface AssistantSelection {\n /** The list the records were picked from, as the person knows it (「계약 대장」). */\n title: string\n /** The list's query (`contracts`) - which list the ids belong to, for the model. */\n list?: string\n records: { id: string; label?: string }[]\n}\n\nexport type SelectionTranslate = (key: string, defaultValue: string, options?: Record<string, unknown>) => string\n\n/** The note stored in the conversation - the person's line, then the key line. Empty when nothing was picked. */\nexport function selectionNote(selection: AssistantSelection, t: SelectionTranslate): string {\n const seen = new Set<string>()\n const records = (selection?.records ?? []).filter(one => one?.id && !seen.has(one.id) && !!seen.add(one.id))\n if (!records.length) return ''\n const labels = records.map(one => (one.label || '').replace(/\\s+/g, ' ').trim()).filter(Boolean)\n const shown = labels.slice(0, SELECTION_LABELS_SHOWN).join(', ')\n const rest = labels.length - Math.min(labels.length, SELECTION_LABELS_SHOWN)\n const line = t('selection-note', 'Selected {count} · {title}', { count: records.length, title: selection.title })\n const named = shown ? `${line}: ${shown}${rest > 0 ? ` ${t('selection-more', 'and {count} more', { count: rest })}` : ''}` : line\n const keys = `${SELECTION_KEYS_PREFIX} ${selection.list ? `${selection.list} ` : ''}${records.map(one => one.id).join(' ')}`\n return `${named}\\n${keys}`\n}\n\n/** A note as the conversation draws it - without the line only the model reads. */\nexport function noteForDisplay(content: string): string {\n return (content ?? '')\n .split('\\n')\n .filter(line => !line.startsWith(SELECTION_KEYS_PREFIX))\n .join('\\n')\n .trim()\n}\n"]}
1
+ {"version":3,"file":"assistant-selection.js","sourceRoot":"","sources":["../../client/utils/assistant-selection.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The records a person picked on a screen and handed to the assistant - `askAssistant({ selection })`.\n *\n * The conversation keeps them as a note: one line the person reads (「선택 1건 · 계약 대장: H2022019 · 브라이스톤 IT3.0\n * Website」) and one line the model reads - the list and the ids, after `[keys]`. The screen hides the key line; the\n * model is handed the note as stored. Until 2026-10-24 the screen sent the selection as JSON and the conversation\n * opened on `{\"source\":\"explicit-page-selection\",…}`.\n */\n\n/**\n * The grid's own row picker - a list that hands its rows to the assistant shows it beside `ox-assistant-selection-bar`\n * (data-grist gutter `row-selector`, many at once).\n */\nexport const SELECTION_ROW_PICKER = { type: 'gutter', gutterName: 'row-selector', multiple: true }\n\n/** A picked row as the person reads it, when the screen gives no words of its own - its code or number, then its name. */\nexport function selectionLabel(row: any): string {\n return [row?.code || row?.number, row?.name || row?.title || row?.description]\n .filter(Boolean)\n .join(' · ')\n}\n\n/**\n * The rows a person picked, as the assistant is handed them (`askAssistant({ selection })`): saved rows only (an id, no\n * pending edit), each once, in screen order - never whole rows. Given the list's axis, each id is the record's key\n * `<axis>:<uuid>`, the one that list's read tool takes (read-target.ts `recordIdOfKey`): a bare uuid names no tool, and\n * a model read a contract's id with a dashboard's card tool (2026-10-25, Office 미수금).\n */\nexport function selectedRecords(rows: any[], options: { axis?: string; labelOf?: (row: any) => string } = {}): { id: string; label: string }[] {\n const { axis, labelOf = selectionLabel } = options\n const seen = new Set<string>()\n return (rows ?? [])\n .filter(row => row?.id && !row.__dirty__ && !seen.has(row.id) && !!seen.add(row.id))\n .map(row => ({ id: axis ? `${axis}:${row.id}` : row.id, label: labelOf(row) || '' }))\n}\n\n/** The line the model reads begins with this; the conversation does not draw it. */\nexport const SELECTION_KEYS_PREFIX = '[keys]'\n\n/** Labels named on the person's line - the rest are counted. */\nexport const SELECTION_LABELS_SHOWN = 5\n\nexport interface AssistantSelection {\n /** The list the records were picked from, as the person knows it (「계약 대장」). */\n title: string\n /** The list's query (`contracts`) - which list the ids belong to, for the model. */\n list?: string\n records: { id: string; label?: string }[]\n}\n\nexport type SelectionTranslate = (key: string, defaultValue: string, options?: Record<string, unknown>) => string\n\n/** The note stored in the conversation - the person's line, then the key line. Empty when nothing was picked. */\nexport function selectionNote(selection: AssistantSelection, t: SelectionTranslate): string {\n const seen = new Set<string>()\n const records = (selection?.records ?? []).filter(one => one?.id && !seen.has(one.id) && !!seen.add(one.id))\n if (!records.length) return ''\n const labels = records.map(one => (one.label || '').replace(/\\s+/g, ' ').trim()).filter(Boolean)\n const shown = labels.slice(0, SELECTION_LABELS_SHOWN).join(', ')\n const rest = labels.length - Math.min(labels.length, SELECTION_LABELS_SHOWN)\n const line = t('selection-note', 'Selected {count} · {title}', { count: records.length, title: selection.title })\n const named = shown ? `${line}: ${shown}${rest > 0 ? ` ${t('selection-more', 'and {count} more', { count: rest })}` : ''}` : line\n const keys = `${SELECTION_KEYS_PREFIX} ${selection.list ? `${selection.list} ` : ''}${records.map(one => one.id).join(' ')}`\n return `${named}\\n${keys}`\n}\n\n/** A note as the conversation draws it - without the line only the model reads. */\nexport function noteForDisplay(content: string): string {\n return (content ?? '')\n .split('\\n')\n .filter(line => !line.startsWith(SELECTION_KEYS_PREFIX))\n .join('\\n')\n .trim()\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { noteForDisplay, selectionNote, SELECTION_KEYS_PREFIX } from './assistant-selection';
1
+ import { noteForDisplay, selectedRecords, selectionLabel, selectionNote, SELECTION_KEYS_PREFIX, SELECTION_ROW_PICKER } from './assistant-selection';
2
2
  const t = (_key, defaultValue, options = {}) => defaultValue.replace(/\{(\w+)\}/g, (_, name) => String(options[name]));
3
3
  const contract = (id, label) => ({ id, label });
4
4
  describe('the records picked on a screen, as a note', () => {
@@ -23,4 +23,24 @@ describe('the records picked on a screen, as a note', () => {
23
23
  expect(selectionNote({ title: 'L', records: [] }, t)).toBe('');
24
24
  });
25
25
  });
26
+ describe('the rows a list hands over (ox-assistant-selection-bar)', () => {
27
+ it('★ each row goes as the key its list\'s read tool takes - `<axis>:<uuid>`', () => {
28
+ expect(selectedRecords([{ id: 'u1', code: 'H1', name: 'A' }], { axis: 'contract' })).toEqual([{ id: 'contract:u1', label: 'H1 · A' }]);
29
+ expect(selectedRecords([{ id: 'u1', name: 'A' }])).toEqual([{ id: 'u1', label: 'A' }]);
30
+ });
31
+ it('saved rows only, each once, in screen order; never the whole row', () => {
32
+ const row = { id: 'a', number: 'INV-1', title: 'T', bankDetails: 'secret' };
33
+ const records = selectedRecords([row, row, { id: 'b', __dirty__: 'M', name: 'B' }, { name: 'no id' }, { id: 'c', name: 'C' }]);
34
+ expect(records).toEqual([{ id: 'a', label: 'INV-1 · T' }, { id: 'c', label: 'C' }]);
35
+ expect(JSON.stringify(records)).not.toMatch(/secret/);
36
+ });
37
+ it('a screen may say a row in its own words', () => {
38
+ expect(selectedRecords([{ id: 'q', number: 'Q-7', version: 2 }], { labelOf: row => `${row.number} v${row.version}` })).toEqual([{ id: 'q', label: 'Q-7 v2' }]);
39
+ expect(selectionLabel({ code: 'C', description: 'D' })).toBe('C · D');
40
+ expect(selectionLabel({})).toBe('');
41
+ });
42
+ it('the row picker is the grid\'s own, many at once', () => {
43
+ expect(SELECTION_ROW_PICKER).toEqual({ type: 'gutter', gutterName: 'row-selector', multiple: true });
44
+ });
45
+ });
26
46
  //# sourceMappingURL=assistant-selection.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-selection.test.js","sourceRoot":"","sources":["../../client/utils/assistant-selection.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { noteForDisplay, selectionNote, SELECTION_KEYS_PREFIX } from './assistant-selection'\n\nconst t = (_key: string, defaultValue: string, options: any = {}) => defaultValue.replace(/\\{(\\w+)\\}/g, (_, name) => String(options[name]))\nconst contract = (id: string, label: string) => ({ id, label })\n\ndescribe('the records picked on a screen, as a note', () => {\n it('★ the person reads a line of names; the model reads the list and the ids', () => {\n const note = selectionNote({ title: '계약 대장', list: 'contracts', records: [contract('c-1', 'H2022019 · 브라이스톤 IT3.0 Website')] }, t)\n expect(note).toBe(`Selected 1 · 계약 대장: H2022019 · 브라이스톤 IT3.0 Website\\n${SELECTION_KEYS_PREFIX} contracts c-1`)\n expect(note).not.toMatch(/[{}\"]|explicit-page-selection/)\n })\n\n it('★ the screen draws the person\\'s line only', () => {\n const note = selectionNote({ title: '계약 대장', list: 'contracts', records: [contract('c-1', 'H2022019')] }, t)\n expect(noteForDisplay(note)).toBe('Selected 1 · 계약 대장: H2022019')\n expect(noteForDisplay('a plain note\\nsecond line')).toBe('a plain note\\nsecond line')\n })\n\n it('every id reaches the model; the names are cut after a few, the rest counted', () => {\n const records = Array.from({ length: 7 }, (_, i) => contract(`c-${i}`, `C${i}`))\n const note = selectionNote({ title: 'L', records }, t)\n expect(note.split('\\n')[0]).toBe('Selected 7 · L: C0, C1, C2, C3, C4 and 2 more')\n expect(note.split('\\n')[1]).toBe(`${SELECTION_KEYS_PREFIX} ${records.map(one => one.id).join(' ')}`)\n })\n\n it('a record picked twice is one; nothing picked is no note', () => {\n expect(selectionNote({ title: 'L', records: [contract('a', 'A'), contract('a', 'A')] }, t)).toBe(`Selected 1 · L: A\\n${SELECTION_KEYS_PREFIX} a`)\n expect(selectionNote({ title: 'L', records: [] }, t)).toBe('')\n })\n})\n"]}
1
+ {"version":3,"file":"assistant-selection.test.js","sourceRoot":"","sources":["../../client/utils/assistant-selection.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { noteForDisplay, selectedRecords, selectionLabel, selectionNote, SELECTION_KEYS_PREFIX, SELECTION_ROW_PICKER } from './assistant-selection'\n\nconst t = (_key: string, defaultValue: string, options: any = {}) => defaultValue.replace(/\\{(\\w+)\\}/g, (_, name) => String(options[name]))\nconst contract = (id: string, label: string) => ({ id, label })\n\ndescribe('the records picked on a screen, as a note', () => {\n it('★ the person reads a line of names; the model reads the list and the ids', () => {\n const note = selectionNote({ title: '계약 대장', list: 'contracts', records: [contract('c-1', 'H2022019 · 브라이스톤 IT3.0 Website')] }, t)\n expect(note).toBe(`Selected 1 · 계약 대장: H2022019 · 브라이스톤 IT3.0 Website\\n${SELECTION_KEYS_PREFIX} contracts c-1`)\n expect(note).not.toMatch(/[{}\"]|explicit-page-selection/)\n })\n\n it('★ the screen draws the person\\'s line only', () => {\n const note = selectionNote({ title: '계약 대장', list: 'contracts', records: [contract('c-1', 'H2022019')] }, t)\n expect(noteForDisplay(note)).toBe('Selected 1 · 계약 대장: H2022019')\n expect(noteForDisplay('a plain note\\nsecond line')).toBe('a plain note\\nsecond line')\n })\n\n it('every id reaches the model; the names are cut after a few, the rest counted', () => {\n const records = Array.from({ length: 7 }, (_, i) => contract(`c-${i}`, `C${i}`))\n const note = selectionNote({ title: 'L', records }, t)\n expect(note.split('\\n')[0]).toBe('Selected 7 · L: C0, C1, C2, C3, C4 and 2 more')\n expect(note.split('\\n')[1]).toBe(`${SELECTION_KEYS_PREFIX} ${records.map(one => one.id).join(' ')}`)\n })\n\n it('a record picked twice is one; nothing picked is no note', () => {\n expect(selectionNote({ title: 'L', records: [contract('a', 'A'), contract('a', 'A')] }, t)).toBe(`Selected 1 · L: A\\n${SELECTION_KEYS_PREFIX} a`)\n expect(selectionNote({ title: 'L', records: [] }, t)).toBe('')\n })\n})\n\ndescribe('the rows a list hands over (ox-assistant-selection-bar)', () => {\n it('★ each row goes as the key its list\\'s read tool takes - `<axis>:<uuid>`', () => {\n expect(selectedRecords([{ id: 'u1', code: 'H1', name: 'A' }], { axis: 'contract' })).toEqual([{ id: 'contract:u1', label: 'H1 · A' }])\n expect(selectedRecords([{ id: 'u1', name: 'A' }])).toEqual([{ id: 'u1', label: 'A' }])\n })\n\n it('saved rows only, each once, in screen order; never the whole row', () => {\n const row = { id: 'a', number: 'INV-1', title: 'T', bankDetails: 'secret' }\n const records = selectedRecords([row, row, { id: 'b', __dirty__: 'M', name: 'B' }, { name: 'no id' }, { id: 'c', name: 'C' }])\n expect(records).toEqual([{ id: 'a', label: 'INV-1 · T' }, { id: 'c', label: 'C' }])\n expect(JSON.stringify(records)).not.toMatch(/secret/)\n })\n\n it('a screen may say a row in its own words', () => {\n expect(selectedRecords([{ id: 'q', number: 'Q-7', version: 2 }], { labelOf: row => `${row.number} v${row.version}` })).toEqual([{ id: 'q', label: 'Q-7 v2' }])\n expect(selectionLabel({ code: 'C', description: 'D' })).toBe('C · D')\n expect(selectionLabel({})).toBe('')\n })\n\n it('the row picker is the grid\\'s own, many at once', () => {\n expect(SELECTION_ROW_PICKER).toEqual({ type: 'gutter', gutterName: 'row-selector', multiple: true })\n })\n})\n"]}
@@ -11,3 +11,18 @@ export declare function sessionActions(entry: {
11
11
  setVisibility: boolean;
12
12
  leave: boolean;
13
13
  };
14
+ /**
15
+ * 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
16
+ * ones I started, leave the ones I am only in, hide any (2026-10-25, the user: 「멀티셀렉트로 한번에 지울 수 있게」).
17
+ */
18
+ export declare function bulkActions(entries: {
19
+ id: string;
20
+ participant?: boolean;
21
+ hidden?: boolean;
22
+ mine?: boolean;
23
+ owner?: boolean;
24
+ }[]): {
25
+ remove: string[];
26
+ leave: string[];
27
+ hide: string[];
28
+ };
@@ -18,4 +18,15 @@ export function sessionActions(entry) {
18
18
  leave: !!entry.participant && !entry.owner
19
19
  };
20
20
  }
21
+ /**
22
+ * 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
23
+ * ones I started, leave the ones I am only in, hide any (2026-10-25, the user: 「멀티셀렉트로 한번에 지울 수 있게」).
24
+ */
25
+ export function bulkActions(entries) {
26
+ return {
27
+ remove: entries.filter(entry => sessionActions(entry).remove).map(entry => entry.id),
28
+ leave: entries.filter(entry => !sessionActions(entry).remove && sessionActions(entry).leave).map(entry => entry.id),
29
+ hide: entries.filter(entry => sessionActions(entry).hide).map(entry => entry.id)
30
+ };
31
+ }
21
32
  //# sourceMappingURL=assistant-session-actions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-session-actions.js","sourceRoot":"","sources":["../../client/utils/assistant-session-actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAC,KAAmF;IAChH,OAAO;QACL,MAAM,EAAE,KAAK,CAAC,WAAW,KAAK,KAAK;QACnC,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM;QACnB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM;QACtB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI;QACpB,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK;QAC5B,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,KAAK;KAC3C,CAAA;AACH,CAAC","sourcesContent":["/*\n * Which actions a row of the conversation list offers this viewer - pure, so the rule is tested alone.\n *\n * The same hints the server gives on each inbox entry; the doors decide again:\n * rename any participant (the name is the topic label; reversible)\n * hide/unhide anyone, for their own list only (reversible)\n * delete the creator (`mine`) (not reversible from a screen)\n * visibility the owner (`owner`) (opening shows what is already written)\n * leave a participant who is not the owner (the owner deletes or makes it private instead)\n */\nexport function sessionActions(entry: { participant?: boolean; hidden?: boolean; mine?: boolean; owner?: boolean }) {\n return {\n rename: entry.participant !== false,\n hide: !entry.hidden,\n unhide: !!entry.hidden,\n remove: !!entry.mine,\n setVisibility: !!entry.owner,\n leave: !!entry.participant && !entry.owner\n }\n}\n"]}
1
+ {"version":3,"file":"assistant-session-actions.js","sourceRoot":"","sources":["../../client/utils/assistant-session-actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAC,KAAmF;IAChH,OAAO;QACL,MAAM,EAAE,KAAK,CAAC,WAAW,KAAK,KAAK;QACnC,IAAI,EAAE,CAAC,KAAK,CAAC,MAAM;QACnB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM;QACtB,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI;QACpB,aAAa,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK;QAC5B,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,KAAK;KAC3C,CAAA;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,OAAmG;IAC7H,OAAO;QACL,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;QACpF,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,MAAM,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;QACnH,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;KACjF,CAAA;AACH,CAAC","sourcesContent":["/*\n * Which actions a row of the conversation list offers this viewer - pure, so the rule is tested alone.\n *\n * The same hints the server gives on each inbox entry; the doors decide again:\n * rename any participant (the name is the topic label; reversible)\n * hide/unhide anyone, for their own list only (reversible)\n * delete the creator (`mine`) (not reversible from a screen)\n * visibility the owner (`owner`) (opening shows what is already written)\n * leave a participant who is not the owner (the owner deletes or makes it private instead)\n */\nexport function sessionActions(entry: { participant?: boolean; hidden?: boolean; mine?: boolean; owner?: boolean }) {\n return {\n rename: entry.participant !== false,\n hide: !entry.hidden,\n unhide: !!entry.hidden,\n remove: !!entry.mine,\n setVisibility: !!entry.owner,\n leave: !!entry.participant && !entry.owner\n }\n}\n\n/**\n * 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\n * ones I started, leave the ones I am only in, hide any (2026-10-25, the user: 「멀티셀렉트로 한번에 지울 수 있게」).\n */\nexport function bulkActions(entries: { id: string; participant?: boolean; hidden?: boolean; mine?: boolean; owner?: boolean }[]) {\n return {\n remove: entries.filter(entry => sessionActions(entry).remove).map(entry => entry.id),\n leave: entries.filter(entry => !sessionActions(entry).remove && sessionActions(entry).leave).map(entry => entry.id),\n hide: entries.filter(entry => sessionActions(entry).hide).map(entry => entry.id)\n }\n}\n"]}
@@ -1,4 +1,4 @@
1
- export { sessionActions } from './assistant-session-actions.js';
1
+ export { bulkActions, sessionActions } from './assistant-session-actions.js';
2
2
  /**
3
3
  * Managing the conversations on one anchor - list, rename, hide, delete, and who may read them.
4
4
  *
@@ -13,6 +13,7 @@ export { sessionActions } from './assistant-session-actions.js';
13
13
  * delete the creator (`mine`) (not reversible from a screen)
14
14
  * visibility the owner (`owner`) (opening shows what is already written)
15
15
  * leave a member, never the owner (in a group, leaving ends reading here)
16
+ * restore the creator, while it is kept (deleted ones are kept `deletedDays`, shared-dock.md §9-5)
16
17
  */
17
18
  /** Who reads it: everyone in the domain, the people the owner brought in, or the owner alone (ADR-0073 decision 3). */
18
19
  export type SessionVisibility = 'open' | 'group' | 'private';
@@ -33,6 +34,25 @@ export interface AssistantSessionEntry {
33
34
  /** Hidden from my own list (returned only with the hidden filter on). */
34
35
  hidden?: boolean;
35
36
  visibility?: SessionVisibility | string;
37
+ /** No activity for the archive period - out of the list until asked for or searched. */
38
+ archived?: boolean;
39
+ /** Why the list suggests clearing it (mine only): never asked anything, or idle for 30 days. */
40
+ cleanupReason?: 'empty' | 'inactive' | string;
41
+ /** Whole days since the last activity. */
42
+ idleDays?: number;
43
+ }
44
+ /** A conversation I deleted that is still kept - it can be brought back until `purgesAt`. */
45
+ export interface DeletedSessionEntry {
46
+ id: string;
47
+ name?: string;
48
+ kind?: string;
49
+ deletedAt: string;
50
+ purgesAt?: string;
51
+ }
52
+ /** How long conversations are kept; `null` when that keeping is off. */
53
+ export interface SessionKeeping {
54
+ deletedDays: number | null;
55
+ archiveDays: number | null;
36
56
  }
37
57
  /** Legacy single filter - kept for callers written before the filter set. */
38
58
  export type AssistantSessionFilter = 'all' | 'mine' | 'hidden';
@@ -44,6 +64,10 @@ export interface AssistantSessionFilters {
44
64
  visibility?: 'all' | SessionVisibility;
45
65
  includeHidden?: boolean;
46
66
  unreadOnly?: boolean;
67
+ /** Also the archived ones. */
68
+ includeArchived?: boolean;
69
+ /** Only my own the list suggests clearing - hidden and archived ones included. */
70
+ cleanupOnly?: boolean;
47
71
  }
48
72
  export interface AssistantSessionQuery {
49
73
  anchorType: string;
@@ -60,6 +84,11 @@ export interface AssistantSessionPage {
60
84
  total: number;
61
85
  /** How many I hid - so a hidden conversation is never forgotten. */
62
86
  hiddenCount: number;
87
+ /** How many are archived - the same, for the ones left idle. */
88
+ archivedCount: number;
89
+ /** How many of mine the list suggests clearing. */
90
+ cleanupCount: number;
91
+ keeping: SessionKeeping;
63
92
  }
64
93
  export interface AssistantSessionAdmin {
65
94
  inbox(query: AssistantSessionQuery): Promise<AssistantSessionPage>;
@@ -68,6 +97,8 @@ export interface AssistantSessionAdmin {
68
97
  remove(sessionId: string): Promise<boolean>;
69
98
  setVisibility(sessionId: string, visibility: SessionVisibility): Promise<boolean>;
70
99
  leave(sessionId: string): Promise<boolean>;
100
+ deleted(anchorType: string, anchorId: string): Promise<DeletedSessionEntry[]>;
101
+ restore(sessionId: string): Promise<boolean>;
71
102
  }
72
103
  /** The page size the server allows at most (`chatSessionInbox` clamps to 200). */
73
104
  export declare const SESSION_PAGE_MAX = 200;
@@ -1,7 +1,7 @@
1
1
  import { client } from '@operato/graphql';
2
2
  import { i18next } from '@operato/i18n';
3
3
  import gql from 'graphql-tag';
4
- export { sessionActions } from './assistant-session-actions.js';
4
+ export { bulkActions, sessionActions } from './assistant-session-actions.js';
5
5
  /** The page size the server allows at most (`chatSessionInbox` clamps to 200). */
6
6
  export const SESSION_PAGE_MAX = 200;
7
7
  async function mutate(mutation, variables, field) {
@@ -17,10 +17,13 @@ export const assistantSessionAdmin = {
17
17
  const mine = filters.whose ? filters.whose === 'mine' : filter === 'mine';
18
18
  const result = await client.query({
19
19
  query: gql `
20
- query ($t: String!, $i: String!, $h: Boolean, $q: String, $m: Boolean, $l: Int, $o: Int, $k: String, $p: Boolean, $v: String, $u: Boolean) {
20
+ 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) {
21
21
  hiddenSessionCount(anchorType: $t, anchorId: $i)
22
- chatSessionInboxTotal(anchorType: $t, anchorId: $i, showHidden: $h, search: $q, mineOnly: $m, kind: $k, participantOnly: $p, visibility: $v, unreadOnly: $u)
23
- chatSessionInbox(anchorType: $t, anchorId: $i, showHidden: $h, search: $q, mineOnly: $m, kind: $k, participantOnly: $p, visibility: $v, unreadOnly: $u, limit: $l, offset: $o) {
22
+ archivedSessionCount(anchorType: $t, anchorId: $i)
23
+ cleanupSessionCount(anchorType: $t, anchorId: $i)
24
+ aiSessionKeeping { deletedDays archiveDays }
25
+ chatSessionInboxTotal(anchorType: $t, anchorId: $i, showHidden: $h, search: $q, mineOnly: $m, kind: $k, participantOnly: $p, visibility: $v, unreadOnly: $u, includeArchived: $a, cleanupOnly: $c)
26
+ 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) {
24
27
  id
25
28
  name
26
29
  kind
@@ -33,6 +36,9 @@ export const assistantSessionAdmin = {
33
36
  owner
34
37
  hidden
35
38
  visibility
39
+ archived
40
+ cleanupReason
41
+ idleDays
36
42
  }
37
43
  }
38
44
  `,
@@ -47,6 +53,8 @@ export const assistantSessionAdmin = {
47
53
  p: filters.whose === 'participant' || null,
48
54
  v: filters.visibility && filters.visibility !== 'all' ? filters.visibility : null,
49
55
  u: filters.unreadOnly || null,
56
+ a: filters.includeArchived || null,
57
+ c: filters.cleanupOnly || null,
50
58
  l: Math.min(SESSION_PAGE_MAX, Math.max(1, limit)),
51
59
  o: Math.max(0, offset)
52
60
  },
@@ -62,7 +70,13 @@ export const assistantSessionAdmin = {
62
70
  /* With the hidden filter the server returns hidden ones alongside the rest, marked - the total counts both. */
63
71
  entries,
64
72
  total: result.data?.chatSessionInboxTotal ?? entries.length,
65
- hiddenCount: result.data?.hiddenSessionCount ?? 0
73
+ hiddenCount: result.data?.hiddenSessionCount ?? 0,
74
+ archivedCount: result.data?.archivedSessionCount ?? 0,
75
+ cleanupCount: result.data?.cleanupSessionCount ?? 0,
76
+ keeping: {
77
+ deletedDays: result.data?.aiSessionKeeping?.deletedDays ?? null,
78
+ archiveDays: result.data?.aiSessionKeeping?.archiveDays ?? null
79
+ }
66
80
  };
67
81
  },
68
82
  rename(sessionId, name) {
@@ -79,6 +93,23 @@ export const assistantSessionAdmin = {
79
93
  },
80
94
  leave(sessionId) {
81
95
  return mutate(gql `mutation ($id: String!) { leaveAISession(sessionId: $id) }`, { id: sessionId }, 'leaveAISession');
96
+ },
97
+ async deleted(anchorType, anchorId) {
98
+ const result = await client.query({
99
+ query: gql `
100
+ query ($t: String!, $i: String!) {
101
+ deletedAISessions(anchorType: $t, anchorId: $i) { id name kind deletedAt purgesAt }
102
+ }
103
+ `,
104
+ variables: { t: anchorType, i: anchorId },
105
+ fetchPolicy: 'no-cache'
106
+ });
107
+ if (result.errors?.length)
108
+ throw new Error(result.errors[0].message);
109
+ return result.data?.deletedAISessions ?? [];
110
+ },
111
+ restore(sessionId) {
112
+ return mutate(gql `mutation ($id: String!) { restoreAISession(sessionId: $id) }`, { id: sessionId }, 'restoreAISession');
82
113
  }
83
114
  };
84
115
  //# sourceMappingURL=assistant-session-admin.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-session-admin.js","sourceRoot":"","sources":["../../client/utils/assistant-session-admin.ts"],"names":[],"mappings":"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","sourcesContent":["import { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport gql from 'graphql-tag'\n\nexport { sessionActions } from './assistant-session-actions.js'\n\n/**\n * Managing the conversations on one anchor - list, rename, hide, delete, and who may read them.\n *\n * These were written once, inside operato-twin's dock (things-factory `b3d1081f4c`, `b44c986c07`,\n * 2026-07-30), and every other dock (plant, figure, warehouse, ecs) had only the tabs. They are the\n * framework's doors (`chatSessionInbox` · `renameAISession` · `hideAISession` · `deleteAISession` ·\n * `setSessionVisibility`), so the screen for them belongs here too - one list every dock opens.\n *\n * Who may do what is the server's to decide; the entry carries hints only:\n * rename any participant (the name is the topic label; reversible)\n * hide/unhide anyone, for their own list only (reversible)\n * delete the creator (`mine`) (not reversible from a screen)\n * visibility the owner (`owner`) (opening shows what is already written)\n * leave a member, never the owner (in a group, leaving ends reading here)\n */\n\n/** Who reads it: everyone in the domain, the people the owner brought in, or the owner alone (ADR-0073 decision 3). */\nexport type SessionVisibility = 'open' | 'group' | 'private'\n\n/** One row of `chatSessionInbox`, as the list draws it. */\nexport interface AssistantSessionEntry {\n id: string\n name?: string\n kind?: string\n lastMessageAt?: string\n lastMessagePreview?: string\n previewRedacted?: boolean\n unread?: boolean\n participant?: boolean\n /** I created it - only then may I delete it. */\n mine?: boolean\n /** I own it - only then may I change who reads it. */\n owner?: boolean\n /** Hidden from my own list (returned only with the hidden filter on). */\n hidden?: boolean\n visibility?: SessionVisibility | string\n}\n\n/** Legacy single filter - kept for callers written before the filter set. */\nexport type AssistantSessionFilter = 'all' | 'mine' | 'hidden'\n\n/** What the manager narrows by (design/ai/shared-dock.md §2-6). Defaults: this kind, everyone's, any visibility, hidden out. */\nexport interface AssistantSessionFilters {\n /** A kind (mode) to list; empty lists every kind on the anchor. */\n kind?: string\n whose?: 'all' | 'mine' | 'participant'\n visibility?: 'all' | SessionVisibility\n includeHidden?: boolean\n unreadOnly?: boolean\n}\n\nexport interface AssistantSessionQuery {\n anchorType: string\n anchorId: string\n search?: string\n filter?: AssistantSessionFilter\n filters?: AssistantSessionFilters\n limit?: number\n offset?: number\n}\n\nexport interface AssistantSessionPage {\n entries: AssistantSessionEntry[]\n /** How many pass the filter - so \"load more\" knows whether the end of the list is the end of the data. */\n total: number\n /** How many I hid - so a hidden conversation is never forgotten. */\n hiddenCount: number\n}\n\nexport interface AssistantSessionAdmin {\n inbox(query: AssistantSessionQuery): Promise<AssistantSessionPage>\n rename(sessionId: string, name: string): Promise<boolean>\n hide(sessionId: string, hidden: boolean): Promise<boolean>\n remove(sessionId: string): Promise<boolean>\n setVisibility(sessionId: string, visibility: SessionVisibility): Promise<boolean>\n leave(sessionId: string): Promise<boolean>\n}\n\n/** The page size the server allows at most (`chatSessionInbox` clamps to 200). */\nexport const SESSION_PAGE_MAX = 200\n\nasync function mutate(mutation: any, variables: Record<string, unknown>, field: string): Promise<boolean> {\n const result = await client.mutate({ mutation, variables })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n return result.data?.[field] === true\n}\n\n/** The framework's session doors, as they are. */\nexport const assistantSessionAdmin: AssistantSessionAdmin = {\n async inbox({ anchorType, anchorId, search, filter = 'all', filters = {}, limit = 50, offset = 0 }) {\n const hidden = filters.includeHidden ?? filter === 'hidden'\n const mine = filters.whose ? filters.whose === 'mine' : filter === 'mine'\n const result = await client.query({\n query: gql`\n query ($t: String!, $i: String!, $h: Boolean, $q: String, $m: Boolean, $l: Int, $o: Int, $k: String, $p: Boolean, $v: String, $u: Boolean) {\n hiddenSessionCount(anchorType: $t, anchorId: $i)\n chatSessionInboxTotal(anchorType: $t, anchorId: $i, showHidden: $h, search: $q, mineOnly: $m, kind: $k, participantOnly: $p, visibility: $v, unreadOnly: $u)\n chatSessionInbox(anchorType: $t, anchorId: $i, showHidden: $h, search: $q, mineOnly: $m, kind: $k, participantOnly: $p, visibility: $v, unreadOnly: $u, limit: $l, offset: $o) {\n id\n name\n kind\n lastMessageAt\n lastMessagePreview\n previewRedacted\n unread\n participant\n mine\n owner\n hidden\n visibility\n }\n }\n `,\n variables: {\n t: anchorType,\n i: anchorId,\n /* Hidden ones only when asked - elsewhere they stay out of the list. */\n h: hidden,\n q: search?.trim() || null,\n m: mine,\n k: filters.kind || null,\n p: filters.whose === 'participant' || null,\n v: filters.visibility && filters.visibility !== 'all' ? filters.visibility : null,\n u: filters.unreadOnly || null,\n l: Math.min(SESSION_PAGE_MAX, Math.max(1, limit)),\n o: Math.max(0, offset)\n },\n fetchPolicy: 'no-cache'\n })\n\n if (result.errors?.length) throw new Error(result.errors[0].message)\n const entries = result.data?.chatSessionInbox\n if (!Array.isArray(entries)) {\n throw new Error(i18next.t('ai-assistant.text.sessions-unreadable', { defaultValue: 'could not load the conversation list.' }))\n }\n\n return {\n /* With the hidden filter the server returns hidden ones alongside the rest, marked - the total counts both. */\n entries,\n total: result.data?.chatSessionInboxTotal ?? entries.length,\n hiddenCount: result.data?.hiddenSessionCount ?? 0\n }\n },\n\n rename(sessionId, name) {\n return mutate(gql`mutation ($id: String!, $n: String!) { renameAISession(sessionId: $id, name: $n) }`, { id: sessionId, n: name }, 'renameAISession')\n },\n\n hide(sessionId, hidden) {\n return mutate(gql`mutation ($id: String!, $h: Boolean) { hideAISession(sessionId: $id, hidden: $h) }`, { id: sessionId, h: hidden }, 'hideAISession')\n },\n\n remove(sessionId) {\n return mutate(gql`mutation ($id: String!) { deleteAISession(sessionId: $id) }`, { id: sessionId }, 'deleteAISession')\n },\n\n setVisibility(sessionId, visibility) {\n return mutate(\n gql`mutation ($id: String!, $v: String!) { setSessionVisibility(sessionId: $id, visibility: $v) }`,\n { id: sessionId, v: visibility },\n 'setSessionVisibility'\n )\n },\n\n leave(sessionId) {\n return mutate(gql`mutation ($id: String!) { leaveAISession(sessionId: $id) }`, { id: sessionId }, 'leaveAISession')\n }\n}\n"]}
1
+ {"version":3,"file":"assistant-session-admin.js","sourceRoot":"","sources":["../../client/utils/assistant-session-admin.ts"],"names":[],"mappings":"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","sourcesContent":["import { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport gql from 'graphql-tag'\n\nexport { bulkActions, sessionActions } from './assistant-session-actions.js'\n\n/**\n * Managing the conversations on one anchor - list, rename, hide, delete, and who may read them.\n *\n * These were written once, inside operato-twin's dock (things-factory `b3d1081f4c`, `b44c986c07`,\n * 2026-07-30), and every other dock (plant, figure, warehouse, ecs) had only the tabs. They are the\n * framework's doors (`chatSessionInbox` · `renameAISession` · `hideAISession` · `deleteAISession` ·\n * `setSessionVisibility`), so the screen for them belongs here too - one list every dock opens.\n *\n * Who may do what is the server's to decide; the entry carries hints only:\n * rename any participant (the name is the topic label; reversible)\n * hide/unhide anyone, for their own list only (reversible)\n * delete the creator (`mine`) (not reversible from a screen)\n * visibility the owner (`owner`) (opening shows what is already written)\n * leave a member, never the owner (in a group, leaving ends reading here)\n * restore the creator, while it is kept (deleted ones are kept `deletedDays`, shared-dock.md §9-5)\n */\n\n/** Who reads it: everyone in the domain, the people the owner brought in, or the owner alone (ADR-0073 decision 3). */\nexport type SessionVisibility = 'open' | 'group' | 'private'\n\n/** One row of `chatSessionInbox`, as the list draws it. */\nexport interface AssistantSessionEntry {\n id: string\n name?: string\n kind?: string\n lastMessageAt?: string\n lastMessagePreview?: string\n previewRedacted?: boolean\n unread?: boolean\n participant?: boolean\n /** I created it - only then may I delete it. */\n mine?: boolean\n /** I own it - only then may I change who reads it. */\n owner?: boolean\n /** Hidden from my own list (returned only with the hidden filter on). */\n hidden?: boolean\n visibility?: SessionVisibility | string\n /** No activity for the archive period - out of the list until asked for or searched. */\n archived?: boolean\n /** Why the list suggests clearing it (mine only): never asked anything, or idle for 30 days. */\n cleanupReason?: 'empty' | 'inactive' | string\n /** Whole days since the last activity. */\n idleDays?: number\n}\n\n/** A conversation I deleted that is still kept - it can be brought back until `purgesAt`. */\nexport interface DeletedSessionEntry {\n id: string\n name?: string\n kind?: string\n deletedAt: string\n purgesAt?: string\n}\n\n/** How long conversations are kept; `null` when that keeping is off. */\nexport interface SessionKeeping {\n deletedDays: number | null\n archiveDays: number | null\n}\n\n/** Legacy single filter - kept for callers written before the filter set. */\nexport type AssistantSessionFilter = 'all' | 'mine' | 'hidden'\n\n/** What the manager narrows by (design/ai/shared-dock.md §2-6). Defaults: this kind, everyone's, any visibility, hidden out. */\nexport interface AssistantSessionFilters {\n /** A kind (mode) to list; empty lists every kind on the anchor. */\n kind?: string\n whose?: 'all' | 'mine' | 'participant'\n visibility?: 'all' | SessionVisibility\n includeHidden?: boolean\n unreadOnly?: boolean\n /** Also the archived ones. */\n includeArchived?: boolean\n /** Only my own the list suggests clearing - hidden and archived ones included. */\n cleanupOnly?: boolean\n}\n\nexport interface AssistantSessionQuery {\n anchorType: string\n anchorId: string\n search?: string\n filter?: AssistantSessionFilter\n filters?: AssistantSessionFilters\n limit?: number\n offset?: number\n}\n\nexport interface AssistantSessionPage {\n entries: AssistantSessionEntry[]\n /** How many pass the filter - so \"load more\" knows whether the end of the list is the end of the data. */\n total: number\n /** How many I hid - so a hidden conversation is never forgotten. */\n hiddenCount: number\n /** How many are archived - the same, for the ones left idle. */\n archivedCount: number\n /** How many of mine the list suggests clearing. */\n cleanupCount: number\n keeping: SessionKeeping\n}\n\nexport interface AssistantSessionAdmin {\n inbox(query: AssistantSessionQuery): Promise<AssistantSessionPage>\n rename(sessionId: string, name: string): Promise<boolean>\n hide(sessionId: string, hidden: boolean): Promise<boolean>\n remove(sessionId: string): Promise<boolean>\n setVisibility(sessionId: string, visibility: SessionVisibility): Promise<boolean>\n leave(sessionId: string): Promise<boolean>\n deleted(anchorType: string, anchorId: string): Promise<DeletedSessionEntry[]>\n restore(sessionId: string): Promise<boolean>\n}\n\n/** The page size the server allows at most (`chatSessionInbox` clamps to 200). */\nexport const SESSION_PAGE_MAX = 200\n\nasync function mutate(mutation: any, variables: Record<string, unknown>, field: string): Promise<boolean> {\n const result = await client.mutate({ mutation, variables })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n return result.data?.[field] === true\n}\n\n/** The framework's session doors, as they are. */\nexport const assistantSessionAdmin: AssistantSessionAdmin = {\n async inbox({ anchorType, anchorId, search, filter = 'all', filters = {}, limit = 50, offset = 0 }) {\n const hidden = filters.includeHidden ?? filter === 'hidden'\n const mine = filters.whose ? filters.whose === 'mine' : filter === 'mine'\n const result = await client.query({\n query: gql`\n 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) {\n hiddenSessionCount(anchorType: $t, anchorId: $i)\n archivedSessionCount(anchorType: $t, anchorId: $i)\n cleanupSessionCount(anchorType: $t, anchorId: $i)\n aiSessionKeeping { deletedDays archiveDays }\n chatSessionInboxTotal(anchorType: $t, anchorId: $i, showHidden: $h, search: $q, mineOnly: $m, kind: $k, participantOnly: $p, visibility: $v, unreadOnly: $u, includeArchived: $a, cleanupOnly: $c)\n 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) {\n id\n name\n kind\n lastMessageAt\n lastMessagePreview\n previewRedacted\n unread\n participant\n mine\n owner\n hidden\n visibility\n archived\n cleanupReason\n idleDays\n }\n }\n `,\n variables: {\n t: anchorType,\n i: anchorId,\n /* Hidden ones only when asked - elsewhere they stay out of the list. */\n h: hidden,\n q: search?.trim() || null,\n m: mine,\n k: filters.kind || null,\n p: filters.whose === 'participant' || null,\n v: filters.visibility && filters.visibility !== 'all' ? filters.visibility : null,\n u: filters.unreadOnly || null,\n a: filters.includeArchived || null,\n c: filters.cleanupOnly || null,\n l: Math.min(SESSION_PAGE_MAX, Math.max(1, limit)),\n o: Math.max(0, offset)\n },\n fetchPolicy: 'no-cache'\n })\n\n if (result.errors?.length) throw new Error(result.errors[0].message)\n const entries = result.data?.chatSessionInbox\n if (!Array.isArray(entries)) {\n throw new Error(i18next.t('ai-assistant.text.sessions-unreadable', { defaultValue: 'could not load the conversation list.' }))\n }\n\n return {\n /* With the hidden filter the server returns hidden ones alongside the rest, marked - the total counts both. */\n entries,\n total: result.data?.chatSessionInboxTotal ?? entries.length,\n hiddenCount: result.data?.hiddenSessionCount ?? 0,\n archivedCount: result.data?.archivedSessionCount ?? 0,\n cleanupCount: result.data?.cleanupSessionCount ?? 0,\n keeping: {\n deletedDays: result.data?.aiSessionKeeping?.deletedDays ?? null,\n archiveDays: result.data?.aiSessionKeeping?.archiveDays ?? null\n }\n }\n },\n\n rename(sessionId, name) {\n return mutate(gql`mutation ($id: String!, $n: String!) { renameAISession(sessionId: $id, name: $n) }`, { id: sessionId, n: name }, 'renameAISession')\n },\n\n hide(sessionId, hidden) {\n return mutate(gql`mutation ($id: String!, $h: Boolean) { hideAISession(sessionId: $id, hidden: $h) }`, { id: sessionId, h: hidden }, 'hideAISession')\n },\n\n remove(sessionId) {\n return mutate(gql`mutation ($id: String!) { deleteAISession(sessionId: $id) }`, { id: sessionId }, 'deleteAISession')\n },\n\n setVisibility(sessionId, visibility) {\n return mutate(\n gql`mutation ($id: String!, $v: String!) { setSessionVisibility(sessionId: $id, visibility: $v) }`,\n { id: sessionId, v: visibility },\n 'setSessionVisibility'\n )\n },\n\n leave(sessionId) {\n return mutate(gql`mutation ($id: String!) { leaveAISession(sessionId: $id) }`, { id: sessionId }, 'leaveAISession')\n },\n\n async deleted(anchorType, anchorId) {\n const result = await client.query({\n query: gql`\n query ($t: String!, $i: String!) {\n deletedAISessions(anchorType: $t, anchorId: $i) { id name kind deletedAt purgesAt }\n }\n `,\n variables: { t: anchorType, i: anchorId },\n fetchPolicy: 'no-cache'\n })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n return result.data?.deletedAISessions ?? []\n },\n\n restore(sessionId) {\n return mutate(gql`mutation ($id: String!) { restoreAISession(sessionId: $id) }`, { id: sessionId }, 'restoreAISession')\n }\n}\n"]}
@@ -320,7 +320,7 @@ let AssistantChatResolver = class AssistantChatResolver {
320
320
  state: context.state
321
321
  })
322
322
  : undefined;
323
- const systemPrompt = subjectLine ? `${settings.systemPrompt}\n\n${subjectLine}` : settings.systemPrompt;
323
+ const systemPrompt = (0, mode_categories_js_1.turnSystemPrompt)(settings.systemPrompt, { subjectLine, page: input.page });
324
324
  const lastUserMessage = input.messages[input.messages.length - 1];
325
325
  let userMessageId;
326
326
  /*
@@ -493,13 +493,15 @@ let AssistantChatResolver = class AssistantChatResolver {
493
493
  systemPrompt,
494
494
  toolCategories: settings.toolCategories,
495
495
  /*
496
- * What goes in front when the mode's tools do not fit in one call (ADR-0061 decision 9): the open page's and the
497
- * anchored record's, then what this conversation used. The anchor is the stored one, never the client's.
496
+ * What goes in front when the mode's tools do not fit in one call (ADR-0061 decision 9): the open page's, the
497
+ * anchored record's and the latest selection's, then what this conversation used. The anchor and the selection are
498
+ * the stored ones, never the client's.
498
499
  */
499
500
  front: {
500
501
  categories: (0, mode_categories_js_1.modeFrontCategories)((0, registry_js_1.getAssistantMode)(input.mode.kind) ?? {}, {
501
502
  page: input.page,
502
- subjectAxis: session?.anchorType === 'subject' ? String(session.anchorId ?? '').split(':')[0] || undefined : undefined
503
+ subjectAxis: session?.anchorType === 'subject' ? String(session.anchorId ?? '').split(':')[0] || undefined : undefined,
504
+ selectionAxes: (0, mode_categories_js_1.selectionAxesOf)(storedRows)
503
505
  }),
504
506
  tools: (0, used_tools_js_1.usedToolNames)(storedRows, [ai_client_base_1.OPEN_TOOLS])
505
507
  },