@things-factory/ai-assistant 10.1.213 → 10.1.214

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 (36) hide show
  1. package/client/components/assistant-selection-bar.test.ts +42 -0
  2. package/client/components/assistant-selection-bar.ts +129 -0
  3. package/client/index.ts +2 -0
  4. package/client/utils/assistant-selection.test.ts +25 -1
  5. package/client/utils/assistant-selection.ts +27 -0
  6. package/dist-client/components/assistant-selection-bar.d.ts +44 -0
  7. package/dist-client/components/assistant-selection-bar.js +149 -0
  8. package/dist-client/components/assistant-selection-bar.js.map +1 -0
  9. package/dist-client/components/assistant-selection-bar.test.d.ts +1 -0
  10. package/dist-client/components/assistant-selection-bar.test.js +38 -0
  11. package/dist-client/components/assistant-selection-bar.test.js.map +1 -0
  12. package/dist-client/index.d.ts +1 -0
  13. package/dist-client/index.js +2 -0
  14. package/dist-client/index.js.map +1 -1
  15. package/dist-client/tsconfig.tsbuildinfo +1 -1
  16. package/dist-client/utils/assistant-selection.d.ts +24 -0
  17. package/dist-client/utils/assistant-selection.js +24 -0
  18. package/dist-client/utils/assistant-selection.js.map +1 -1
  19. package/dist-client/utils/assistant-selection.test.js +21 -1
  20. package/dist-client/utils/assistant-selection.test.js.map +1 -1
  21. package/dist-server/service/assistant-chat-resolver.js +5 -3
  22. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  23. package/dist-server/service/assistant-mode/mode-categories.d.ts +15 -0
  24. package/dist-server/service/assistant-mode/mode-categories.js +28 -1
  25. package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
  26. package/dist-server/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +2 -2
  28. package/server/service/assistant-chat-resolver.ts +6 -4
  29. package/server/service/assistant-mode/mode-categories.ts +31 -1
  30. package/server/service/assistant-mode/mode-front.test.ts +27 -1
  31. package/translations/en.json +8 -1
  32. package/translations/es.json +8 -1
  33. package/translations/ja.json +8 -1
  34. package/translations/ko.json +8 -1
  35. package/translations/ms.json +8 -1
  36. package/translations/zh.json +8 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/ai-assistant",
3
- "version": "10.1.213",
3
+ "version": "10.1.214",
4
4
  "main": "dist-server/index.js",
5
5
  "things-factory": true,
6
6
  "author": "heartyoh",
@@ -55,5 +55,5 @@
55
55
  "copyfiles": "^2.4.1",
56
56
  "rimraf": "^5.0.0"
57
57
  },
58
- "gitHead": "81f23aa18cb584c7d8185e81cca0d688f509cf8c"
58
+ "gitHead": "15a9f9b0b21a3ad50159d4a4f81e861362f6ef72"
59
59
  }
@@ -54,7 +54,7 @@ import { attachmentForModel, describeAttachment, findChatAttachment } from './ch
54
54
  import { readTurnAttachments } from './chat-session/turn-attachments.js'
55
55
  import { getAssistantMode } from './assistant-mode/registry.js'
56
56
  import { resolveModeSettings } from './assistant-mode/resolve.js'
57
- import { modeFrontCategories } from './assistant-mode/mode-categories.js'
57
+ import { modeFrontCategories, selectionAxesOf } from './assistant-mode/mode-categories.js'
58
58
  import { usedToolNames } from './chat-message/used-tools.js'
59
59
  import { splitNextRequest } from './assistant-mode/screen-rules.js'
60
60
  import { requestTranslator } from './request-translator.js'
@@ -511,13 +511,15 @@ export class AssistantChatResolver {
511
511
  systemPrompt,
512
512
  toolCategories: settings.toolCategories,
513
513
  /*
514
- * 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
515
- * anchored record's, then what this conversation used. The anchor is the stored one, never the client's.
514
+ * What goes in front when the mode's tools do not fit in one call (ADR-0061 decision 9): the open page's, the
515
+ * anchored record's and the latest selection's, then what this conversation used. The anchor and the selection are
516
+ * the stored ones, never the client's.
516
517
  */
517
518
  front: {
518
519
  categories: modeFrontCategories(getAssistantMode(input.mode.kind) ?? {}, {
519
520
  page: input.page,
520
- subjectAxis: (session as any)?.anchorType === 'subject' ? String((session as any).anchorId ?? '').split(':')[0] || undefined : undefined
521
+ subjectAxis: (session as any)?.anchorType === 'subject' ? String((session as any).anchorId ?? '').split(':')[0] || undefined : undefined,
522
+ selectionAxes: selectionAxesOf(storedRows as any)
521
523
  }),
522
524
  tools: usedToolNames(storedRows as any, [OPEN_TOOLS])
523
525
  },
@@ -61,6 +61,31 @@ export interface TurnScreen {
61
61
  page?: string
62
62
  /** The axis of the record the conversation is anchored to (`contract` of `subject` · `contract:<id>`), from the stored anchor. */
63
63
  subjectAxis?: string
64
+ /** The axes of the records the latest selection named (`contract` of `[keys] contracts contract:<id>`) - `selectionAxesOf`. */
65
+ selectionAxes?: string[]
66
+ }
67
+
68
+ /** The line of a selection note the model reads - the client's `SELECTION_KEYS_PREFIX` (assistant-selection.ts). */
69
+ export const SELECTION_KEYS_LINE = '[keys]'
70
+
71
+ /**
72
+ * The axes of the records the latest selection note names (「AI와 작업」, shared-dock §2-11-1): a list hands its rows as
73
+ * `<axis>:<uuid>`, so the rows' list is read with the tool of that axis. Put in front like an anchored record's - picked
74
+ * on 미수금, the contracts' read was not in front, and the model looked for them among attachments and a dashboard's cards
75
+ * before it opened the right tool (2026-10-25). A note is the server's (`appendSessionNote`, system); a person's words
76
+ * are never read for it.
77
+ */
78
+ export function selectionAxesOf(rows: { role?: string; content?: string | null }[]): string[] {
79
+ for (let i = rows.length - 1; i >= 0; i--) {
80
+ if (rows[i].role !== 'system') continue
81
+ const line = String(rows[i].content ?? '')
82
+ .split('\n')
83
+ .find(one => one.startsWith(SELECTION_KEYS_LINE))
84
+ if (!line) continue
85
+ const keys = line.slice(SELECTION_KEYS_LINE.length).trim().split(/\s+/)
86
+ return [...new Set(keys.map(one => one.match(/^([a-z0-9-]+):/)?.[1]).filter((one): one is string => !!one))]
87
+ }
88
+ return []
64
89
  }
65
90
 
66
91
  type FrontMode = { toolCategories?: string[]; attachments?: { kinds?: string[] }; entryTargets?: string[]; readTargets?: string[] }
@@ -72,7 +97,12 @@ type FrontMode = { toolCategories?: string[]; attachments?: { kinds?: string[] }
72
97
  */
73
98
  export function screenTargetKeys(mode: FrontMode, screen: TurnScreen, routeOf: (key: string) => string | undefined = key => readTargetOf(key)?.screen?.route): string[] {
74
99
  const keys = [...new Set([...(mode.readTargets ?? []), ...(mode.entryTargets ?? [])])]
75
- return keys.filter(key => (!!screen.page && routeOf(key) === screen.page) || (!!screen.subjectAxis && axesOfKey(mode, key).includes(screen.subjectAxis)))
100
+ return keys.filter(
101
+ key =>
102
+ (!!screen.page && routeOf(key) === screen.page) ||
103
+ (!!screen.subjectAxis && axesOfKey(mode, key).includes(screen.subjectAxis)) ||
104
+ (!!screen.selectionAxes?.length && axesOfKey(mode, key).some(axis => screen.selectionAxes!.includes(axis)))
105
+ )
76
106
  }
77
107
 
78
108
  /** The records a key is about: its last part, and the axes a card joins or a whole answer covers (EntryTarget.axes · DetailRead.axes). */
@@ -4,7 +4,9 @@
4
4
  */
5
5
  import { clearReadTargets, declareDetailRead, declareReadTarget } from '../data-read/read-target'
6
6
  import { listReadTarget } from '../data-read/list-read'
7
- import { modeFrontCategories, modeScreens, screenTargetKeys } from './mode-categories'
7
+ import { modeFrontCategories, modeScreens, screenTargetKeys, selectionAxesOf, SELECTION_KEYS_LINE } from './mode-categories'
8
+ import { readFileSync } from 'node:fs'
9
+ import { join } from 'node:path'
8
10
  import { clearEntryTargets, declareEntryTarget } from '../data-entry/entry-target'
9
11
 
10
12
  const list = (key: string, route?: string) =>
@@ -42,6 +44,30 @@ describe('the open screen decides what goes in front', () => {
42
44
  expect(screenTargetKeys(mode, { subjectAxis: 'contract-round' })).toEqual(['office.contract-round'])
43
45
  })
44
46
 
47
+ it('★ the rows a person picked put their list in front - by the axis of their keys, from the latest note only', () => {
48
+ const rows = [
49
+ { role: 'system', content: 'Selected 1 · 미수금: H1\n[keys] contracts contract:a1' },
50
+ { role: 'user', content: '[keys] loans loan:x - typed by a person, never read for it' },
51
+ { role: 'system', content: 'Selected 2 · 입출금: A, B\n[keys] transactions cash-transaction:b1 cash-transaction:b2' }
52
+ ]
53
+ expect(selectionAxesOf(rows)).toEqual(['cash-transaction'])
54
+ expect(selectionAxesOf(rows.slice(0, 2))).toEqual(['contract'])
55
+ expect(selectionAxesOf([{ role: 'system', content: 'Selected 1 · L\n[keys] rows a1' }])).toEqual([])
56
+ expect(selectionAxesOf([])).toEqual([])
57
+ expect(screenTargetKeys(mode, { selectionAxes: ['contract'] })).toEqual(['office.contract'])
58
+ expect(modeFrontCategories(mode, { page: 'receivable-page', selectionAxes: ['contract'] })).toEqual(expect.arrayContaining(['data-read:office.contract', 'data-entry:office.contract']))
59
+ })
60
+
61
+ it('★ a turn puts the stored conversation\'s selection in front - never the client\'s', () => {
62
+ const resolver = readFileSync(join(__dirname, '../assistant-chat-resolver.ts'), 'utf-8')
63
+ expect(resolver).toMatch(/modeFrontCategories\([\s\S]{0,400}selectionAxes: selectionAxesOf\(storedRows as any\)/)
64
+ })
65
+
66
+ it('the note\'s key line is the one the client writes', () => {
67
+ const client = readFileSync(join(__dirname, '../../../client/utils/assistant-selection.ts'), 'utf-8')
68
+ expect(client).toContain(`export const SELECTION_KEYS_PREFIX = '${SELECTION_KEYS_LINE}'`)
69
+ })
70
+
45
71
  it('★ a record the mode does not read or enter is not put in front', () => {
46
72
  expect(modeFrontCategories({ ...mode, readTargets: [], entryTargets: [] }, { page: 'cash-transaction-list' })).toEqual(['office-ops', 'assistant-workspace', 'data-read:organization.employee', 'chat-invite', 'chat-attachments'])
47
73
  })
@@ -441,5 +441,12 @@
441
441
  "ai-assistant.dock.screen-request-default": "Read the selected records and suggest what can be done with them now.",
442
442
  "ai-assistant.dock.screen-request-failed": "The request was not sent. Try again.",
443
443
  "ai-assistant.text.screen-request-busy": "an answer is still being written.",
444
- "ai-assistant.text.screen-request-no-session": "this screen has no conversation to send to."
444
+ "ai-assistant.text.screen-request-no-session": "this screen has no conversation to send to.",
445
+ "ai-assistant.selection.count": "{n} selected",
446
+ "ai-assistant.selection.request": "Describe what to do with these records",
447
+ "ai-assistant.selection.work-with-ai": "Work with AI",
448
+ "ai-assistant.selection.region": "Work with selection",
449
+ "ai-assistant.selection.pick-rows": "Select rows to work on with AI.",
450
+ "ai-assistant.selection.save-first": "Save or cancel pending edits before working with the selected rows.",
451
+ "ai-assistant.selection.ask": "Read the {n} selected records from {title} and suggest what can be done with them now."
445
452
  }
@@ -441,5 +441,12 @@
441
441
  "ai-assistant.dock.screen-request-default": "Lee los registros seleccionados y sugiere qué se puede hacer ahora.",
442
442
  "ai-assistant.dock.screen-request-failed": "La solicitud no se envió. Inténtalo de nuevo.",
443
443
  "ai-assistant.text.screen-request-busy": "todavía se está escribiendo una respuesta.",
444
- "ai-assistant.text.screen-request-no-session": "esta pantalla no tiene una conversación a la que enviar."
444
+ "ai-assistant.text.screen-request-no-session": "esta pantalla no tiene una conversación a la que enviar.",
445
+ "ai-assistant.selection.count": "{n} seleccionados",
446
+ "ai-assistant.selection.request": "Describe qué hacer con estos registros",
447
+ "ai-assistant.selection.work-with-ai": "Trabajar con IA",
448
+ "ai-assistant.selection.region": "Trabajar con la selección",
449
+ "ai-assistant.selection.pick-rows": "Selecciona primero las filas con las que trabajar con la IA.",
450
+ "ai-assistant.selection.save-first": "Guarda o cancela las ediciones pendientes antes de trabajar con las filas seleccionadas.",
451
+ "ai-assistant.selection.ask": "Lee los {n} registros seleccionados de {title} y sugiere qué se puede hacer con ellos ahora."
445
452
  }
@@ -441,5 +441,12 @@
441
441
  "ai-assistant.dock.screen-request-default": "選択した記録を読んで、今できることをまとめて。",
442
442
  "ai-assistant.dock.screen-request-failed": "リクエストを送れませんでした。もう一度お試しください。",
443
443
  "ai-assistant.text.screen-request-busy": "まだ回答を作成中です。",
444
- "ai-assistant.text.screen-request-no-session": "この画面には送信先の会話がありません。"
444
+ "ai-assistant.text.screen-request-no-session": "この画面には送信先の会話がありません。",
445
+ "ai-assistant.selection.count": "{n}件選択",
446
+ "ai-assistant.selection.request": "選択した項目にすることを入力してください",
447
+ "ai-assistant.selection.work-with-ai": "AIと作業",
448
+ "ai-assistant.selection.region": "選択項目の作業",
449
+ "ai-assistant.selection.pick-rows": "AIと作業する行を先に選択してください。",
450
+ "ai-assistant.selection.save-first": "編集中の内容を保存または取り消してから、選択した行で作業してください。",
451
+ "ai-assistant.selection.ask": "{title}で選択した{n}件を読んで、今できることをすぐ提案して。"
445
452
  }
@@ -441,5 +441,12 @@
441
441
  "ai-assistant.dock.screen-request-default": "선택한 기록을 읽고 지금 할 수 있는 일을 정리해 줘.",
442
442
  "ai-assistant.dock.screen-request-failed": "요청을 보내지 못했습니다. 다시 눌러 주세요.",
443
443
  "ai-assistant.text.screen-request-busy": "아직 답을 쓰는 중입니다.",
444
- "ai-assistant.text.screen-request-no-session": "이 화면에는 보낼 대화가 없습니다."
444
+ "ai-assistant.text.screen-request-no-session": "이 화면에는 보낼 대화가 없습니다.",
445
+ "ai-assistant.selection.count": "{n}건 선택",
446
+ "ai-assistant.selection.request": "선택한 항목에 할 일을 적으세요",
447
+ "ai-assistant.selection.work-with-ai": "AI와 작업",
448
+ "ai-assistant.selection.region": "선택 항목 작업",
449
+ "ai-assistant.selection.pick-rows": "AI와 함께 작업할 행을 먼저 선택하세요.",
450
+ "ai-assistant.selection.save-first": "편집 중인 내용을 먼저 저장하거나 취소한 뒤 선택한 행으로 작업하세요.",
451
+ "ai-assistant.selection.ask": "{title}에서 선택한 {n}건을 읽고 지금 할 수 있는 일을 바로 제안해 줘."
445
452
  }
@@ -441,5 +441,12 @@
441
441
  "ai-assistant.dock.screen-request-default": "Baca rekod yang dipilih dan cadangkan apa yang boleh dibuat sekarang.",
442
442
  "ai-assistant.dock.screen-request-failed": "Permintaan tidak dihantar. Cuba lagi.",
443
443
  "ai-assistant.text.screen-request-busy": "jawapan masih sedang ditulis.",
444
- "ai-assistant.text.screen-request-no-session": "skrin ini tiada perbualan untuk dihantar."
444
+ "ai-assistant.text.screen-request-no-session": "skrin ini tiada perbualan untuk dihantar.",
445
+ "ai-assistant.selection.count": "{n} dipilih",
446
+ "ai-assistant.selection.request": "Tulis apa yang perlu dibuat dengan rekod ini",
447
+ "ai-assistant.selection.work-with-ai": "Bekerja dengan AI",
448
+ "ai-assistant.selection.region": "Kerja dengan pilihan",
449
+ "ai-assistant.selection.pick-rows": "Pilih baris untuk diusahakan bersama AI dahulu.",
450
+ "ai-assistant.selection.save-first": "Simpan atau batalkan suntingan yang belum disimpan sebelum bekerja dengan baris yang dipilih.",
451
+ "ai-assistant.selection.ask": "Baca {n} rekod yang dipilih daripada {title} dan cadangkan apa yang boleh dibuat sekarang."
445
452
  }
@@ -441,5 +441,12 @@
441
441
  "ai-assistant.dock.screen-request-default": "读取所选记录,并整理现在可以做的事。",
442
442
  "ai-assistant.dock.screen-request-failed": "请求未发送,请重试。",
443
443
  "ai-assistant.text.screen-request-busy": "回答仍在生成中。",
444
- "ai-assistant.text.screen-request-no-session": "此页面没有可发送的对话。"
444
+ "ai-assistant.text.screen-request-no-session": "此页面没有可发送的对话。",
445
+ "ai-assistant.selection.count": "已选择 {n} 条",
446
+ "ai-assistant.selection.request": "请写下要对所选记录做什么",
447
+ "ai-assistant.selection.work-with-ai": "与 AI 协作",
448
+ "ai-assistant.selection.region": "所选记录操作",
449
+ "ai-assistant.selection.pick-rows": "请先选择要与 AI 一起处理的行。",
450
+ "ai-assistant.selection.save-first": "请先保存或取消正在编辑的内容,再处理所选行。",
451
+ "ai-assistant.selection.ask": "读取在{title}中选择的{n}条记录,并立即提出现在可以做的事。"
445
452
  }