@things-factory/ai-assistant 10.1.113 → 10.1.116

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 (76) hide show
  1. package/client/components/assistant-chat.ts +19 -0
  2. package/client/components/assistant-dock.ts +26 -3
  3. package/client/setup-assistant-dock.ts +3 -2
  4. package/client/utils/entry-proposal.ts +14 -1
  5. package/client/utils/open-page.ts +21 -0
  6. package/dist-client/components/assistant-chat.d.ts +2 -0
  7. package/dist-client/components/assistant-chat.js +19 -0
  8. package/dist-client/components/assistant-chat.js.map +1 -1
  9. package/dist-client/components/assistant-dock.d.ts +5 -0
  10. package/dist-client/components/assistant-dock.js +25 -3
  11. package/dist-client/components/assistant-dock.js.map +1 -1
  12. package/dist-client/setup-assistant-dock.d.ts +1 -0
  13. package/dist-client/setup-assistant-dock.js +1 -0
  14. package/dist-client/setup-assistant-dock.js.map +1 -1
  15. package/dist-client/tsconfig.tsbuildinfo +1 -1
  16. package/dist-client/utils/entry-proposal.d.ts +10 -1
  17. package/dist-client/utils/entry-proposal.js +13 -1
  18. package/dist-client/utils/entry-proposal.js.map +1 -1
  19. package/dist-client/utils/open-page.d.ts +5 -0
  20. package/dist-client/utils/open-page.js +19 -0
  21. package/dist-client/utils/open-page.js.map +1 -0
  22. package/dist-server/service/assistant-mode/resolve.d.ts +1 -0
  23. package/dist-server/service/assistant-mode/resolve.js +2 -1
  24. package/dist-server/service/assistant-mode/resolve.js.map +1 -1
  25. package/dist-server/service/assistant-mode/types.d.ts +2 -0
  26. package/dist-server/service/assistant-mode/types.js.map +1 -1
  27. package/dist-server/service/chat-session/attachment-tool-result.d.ts +28 -0
  28. package/dist-server/service/chat-session/attachment-tool-result.js +9 -0
  29. package/dist-server/service/chat-session/attachment-tool-result.js.map +1 -0
  30. package/dist-server/service/chat-session/attachment-tools.js +4 -5
  31. package/dist-server/service/chat-session/attachment-tools.js.map +1 -1
  32. package/dist-server/service/data-entry/entry-apply.d.ts +8 -0
  33. package/dist-server/service/data-entry/entry-apply.js +19 -2
  34. package/dist-server/service/data-entry/entry-apply.js.map +1 -1
  35. package/dist-server/service/data-entry/entry-refusal.d.ts +6 -0
  36. package/dist-server/service/data-entry/entry-refusal.js +25 -0
  37. package/dist-server/service/data-entry/entry-refusal.js.map +1 -0
  38. package/dist-server/service/data-entry/entry-resolver.js +2 -0
  39. package/dist-server/service/data-entry/entry-resolver.js.map +1 -1
  40. package/dist-server/service/data-read/list-read.d.ts +36 -0
  41. package/dist-server/service/data-read/list-read.js +64 -0
  42. package/dist-server/service/data-read/list-read.js.map +1 -0
  43. package/dist-server/service/data-read/read-target.d.ts +109 -0
  44. package/dist-server/service/data-read/read-target.js +189 -0
  45. package/dist-server/service/data-read/read-target.js.map +1 -0
  46. package/dist-server/service/data-read/read-tools.d.ts +228 -0
  47. package/dist-server/service/data-read/read-tools.js +197 -0
  48. package/dist-server/service/data-read/read-tools.js.map +1 -0
  49. package/dist-server/service/index.d.ts +3 -0
  50. package/dist-server/service/index.js +7 -1
  51. package/dist-server/service/index.js.map +1 -1
  52. package/dist-server/tsconfig.tsbuildinfo +1 -1
  53. package/package.json +5 -5
  54. package/server/service/assistant-mode/resolve.ts +3 -2
  55. package/server/service/assistant-mode/types.ts +2 -0
  56. package/server/service/chat-session/attachment-tool-result.test.ts +26 -0
  57. package/server/service/chat-session/attachment-tool-result.ts +18 -0
  58. package/server/service/chat-session/attachment-tools.ts +4 -5
  59. package/server/service/data-entry/data-entry-update.test.ts +10 -1
  60. package/server/service/data-entry/data-entry.test.ts +29 -1
  61. package/server/service/data-entry/entry-apply.ts +27 -2
  62. package/server/service/data-entry/entry-refusal.test.ts +73 -0
  63. package/server/service/data-entry/entry-refusal.ts +20 -0
  64. package/server/service/data-entry/entry-resolver.ts +2 -0
  65. package/server/service/data-read/data-read.test.ts +229 -0
  66. package/server/service/data-read/list-read.ts +98 -0
  67. package/server/service/data-read/read-target.ts +230 -0
  68. package/server/service/data-read/read-tools.ts +212 -0
  69. package/server/service/index.ts +4 -0
  70. package/test/entry-proposal.test.ts +31 -1
  71. package/test/open-page.test.ts +31 -0
  72. package/translations/en.json +14 -0
  73. package/translations/ja.json +14 -0
  74. package/translations/ko.json +14 -0
  75. package/translations/ms.json +14 -0
  76. package/translations/zh.json +14 -0
@@ -70,6 +70,7 @@ import { availableTriggers, type TriggerChip } from './chat-triggers.js'
70
70
  import './entry-review.js'
71
71
  import { entryGroupOf, entryReadyCount, entryTitle, isChangeProposal, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js'
72
72
  import { isWorkspaceProposal, versionOfCard, workspaceAddress } from '../utils/workspace.js'
73
+ import { isOpenPageProposal, openPageNote } from '../utils/open-page.js'
73
74
 
74
75
  interface ChatLine {
75
76
  role: 'user' | 'assistant' | 'system'
@@ -4321,6 +4322,8 @@ export class OxAssistantChat extends LitElement {
4321
4322
  const key = this.proposalKey(p)
4322
4323
  /* A workspace card opens the page; it runs nothing (design/ai/workspace.md §4). */
4323
4324
  if (isWorkspaceProposal(p)) return this.renderWorkspaceCard(p, key)
4325
+ /* A screen card opens the application's list with the question's conditions; it runs nothing (design/ai/read-targets.md §5). */
4326
+ if (isOpenPageProposal(p)) return this.renderOpenPageCard(p)
4324
4327
  const card = proposalCardState(key, { outcomes, requested, sentHere: this.sentProposals.has(key) })
4325
4328
  const entry = isEntryProposal(p)
4326
4329
  const replaced = entry && latestEntry?.get(entryGroupOf(p)) !== key && card.state === 'ready'
@@ -4415,6 +4418,22 @@ export class OxAssistantChat extends LitElement {
4415
4418
  `
4416
4419
  }
4417
4420
 
4421
+ /** The screen card - which list, the conditions the screen does not take, and the way to open it. */
4422
+ private renderOpenPageCard(p: any) {
4423
+ const note = openPageNote(p, (key, options) => String(i18next.t(key, options)))
4424
+ const open = () => this.dispatchEvent(new CustomEvent('assistant-open-page', { detail: { href: p.href }, bubbles: true, composed: true }))
4425
+ return html`
4426
+ <div class="proposal workspace">
4427
+ <md-icon>${p.icon || 'open_in_new'}</md-icon>
4428
+ <span class="pbody">
4429
+ <span class="plabel">${p.label}</span>
4430
+ ${note ? html`<span class="preason">${note}</span>` : nothing}
4431
+ </span>
4432
+ <button class="run" @click=${open}>${i18next.t('ai-assistant.screen.open', { defaultValue: 'open the screen' })}</button>
4433
+ </div>
4434
+ `
4435
+ }
4436
+
4418
4437
  /** The AI 작업판 card - what it holds, which version, and the way to open it. The person opens it; the model does not. */
4419
4438
  private renderWorkspaceCard(p: any, key: string) {
4420
4439
  const { version, of } = versionOfCard(this.lines.map(one => one.proposals ?? []), p.workspace.name, key)
@@ -109,9 +109,9 @@ export class AssistantDock extends localize(i18next)(LitElement) {
109
109
  @media (max-width: 1439.98px) {
110
110
  :host {
111
111
  position: fixed;
112
- inset: 0 0 0 auto;
113
- height: 100vh;
114
- height: 100dvh;
112
+ inset: var(--assistant-dock-overlay-top, 0px) 0 0 auto;
113
+ height: calc(100vh - var(--assistant-dock-overlay-top, 0px));
114
+ height: calc(100dvh - var(--assistant-dock-overlay-top, 0px));
115
115
  z-index: 10;
116
116
  box-shadow: var(--md-sys-elevation-shadow-3, -4px 0 16px rgb(0 0 0 / 20%));
117
117
  }
@@ -329,6 +329,23 @@ export class AssistantDock extends localize(i18next)(LitElement) {
329
329
 
330
330
  private onStartNewSession = () => void this.newConversation()
331
331
 
332
+ /** An editor can reserve its toolbar above the floating dock. Other apps keep the full-height default. */
333
+ @property({ attribute: false }) overlayBoundary?: () => Element | undefined
334
+ private boundary?: Element
335
+ private boundaryObserver?: ResizeObserver
336
+ private placeOverlay = () => {
337
+ const boundary = this.overlayBoundary?.()
338
+ if (boundary !== this.boundary) {
339
+ this.boundaryObserver?.disconnect()
340
+ this.boundary = boundary
341
+ if (boundary) {
342
+ this.boundaryObserver = new ResizeObserver(this.placeOverlay)
343
+ this.boundaryObserver.observe(boundary)
344
+ }
345
+ }
346
+ this.style.setProperty('--assistant-dock-overlay-top', `${boundary?.getBoundingClientRect().bottom ?? 0}px`)
347
+ }
348
+
332
349
  /* A card asks for a page of this application - the AI 작업판. The dock stays open beside it. */
333
350
  private onOpenPage = (event: CustomEvent) => {
334
351
  const href = event.detail?.href
@@ -349,6 +366,8 @@ export class AssistantDock extends localize(i18next)(LitElement) {
349
366
 
350
367
  connectedCallback() {
351
368
  super.connectedCallback()
369
+ window.addEventListener('resize', this.placeOverlay)
370
+ this.placeOverlay()
352
371
  this.addEventListener('assistant-start-new-session', this.onStartNewSession)
353
372
  this.addEventListener('assistant-open-page', this.onOpenPage as EventListener)
354
373
  this.unsubscribers.push(
@@ -371,6 +390,9 @@ export class AssistantDock extends localize(i18next)(LitElement) {
371
390
 
372
391
  /* Closing keeps the conversation: the dock comes back as it was left (design/ai/shared-dock.md §2-1, §2-7). */
373
392
  disconnectedCallback() {
393
+ window.removeEventListener('resize', this.placeOverlay)
394
+ this.boundaryObserver?.disconnect()
395
+ this.boundary = undefined
374
396
  this.removeEventListener('assistant-start-new-session', this.onStartNewSession)
375
397
  this.removeEventListener('assistant-open-page', this.onOpenPage as EventListener)
376
398
  this.unsubscribers.splice(0).forEach(unsubscribe => unsubscribe())
@@ -417,6 +439,7 @@ export class AssistantDock extends localize(i18next)(LitElement) {
417
439
 
418
440
  /** The screen changed - a different surface or domain is a different conversation. */
419
441
  private follow(force = false) {
442
+ this.placeOverlay()
420
443
  const next = currentAssistantSurface()
421
444
  const transition = surfaceTransition({
422
445
  identityChanged: this.openedIdentity !== this.identityKey(),
@@ -117,7 +117,7 @@ export class AssistantDockButton extends LitElement {
117
117
 
118
118
  let installed = false
119
119
 
120
- export function setupAssistantDock(options: { viewpart?: string } = {}): void {
120
+ export function setupAssistantDock(options: { viewpart?: string; overlayBoundary?: () => Element | undefined } = {}): void {
121
121
  if (installed) return
122
122
  installed = true
123
123
  const viewpart = options.viewpart ?? ASSISTANT_DOCK_VIEWPART
@@ -129,8 +129,9 @@ export function setupAssistantDock(options: { viewpart?: string } = {}): void {
129
129
  * makes the element would make a new dock on every opening - and a new composer, the text being written gone
130
130
  * (design/ai/shared-dock.md §2-1, §2-7). The same element is put back instead, as the entity360 dock is.
131
131
  */
132
- const dock = document.createElement('ox-assistant-dock') as HTMLElement & { viewpart: string }
132
+ const dock = document.createElement('ox-assistant-dock') as HTMLElement & { viewpart: string; overlayBoundary?: () => Element | undefined }
133
133
  dock.viewpart = viewpart
134
+ dock.overlayBoundary = options.overlayBoundary
134
135
 
135
136
  appendViewpart({
136
137
  name: viewpart,
@@ -46,6 +46,7 @@ export interface EntryRowResult {
46
46
 
47
47
  export interface EntryApplyAnswer extends ProposalOutcome {
48
48
  rows?: EntryRowResult[]
49
+ refusal?: { message: string; extensions: Record<string, unknown>; path: string[] }
49
50
  /** Set by the handler from the card - which words the conversation line uses. */
50
51
  op?: string
51
52
  }
@@ -96,7 +97,12 @@ export function entryOutcomeNote(answer: EntryApplyAnswer, t: (key: string, opti
96
97
  export function entryProposalHandler(
97
98
  apply: (sessionId: string, proposalKey: string, reviewedHash?: string) => Promise<EntryApplyAnswer>,
98
99
  t: (key: string, options?: any) => string,
99
- reviewedOf: (key: string) => string | undefined = reviewedHashOf
100
+ reviewedOf: (key: string) => string | undefined = reviewedHashOf,
101
+ reportRefusal: (notification: { level: 'error'; message: string; ex: NonNullable<EntryApplyAnswer['refusal']>[] }) => void | Promise<void> = async notification => {
102
+ /* The generic toast transport. auth-ui alone interprets the refusal and offers an access request. */
103
+ const { notify } = await import('@operato/layout')
104
+ notify(notification)
105
+ }
100
106
  ) {
101
107
  return async (proposal: any, context: ProposalContext): Promise<ProposalOutcome> => {
102
108
  if (!context.sessionId || !proposal?.key) return { verdict: 'unavailable', errorCode: 'no-conversation' }
@@ -104,6 +110,13 @@ export function entryProposalHandler(
104
110
  const reviewed = reviewedOf(proposal.key)
105
111
  if (!reviewStillHolds(proposal, reviewed)) return { verdict: 'refused', errorCode: 'review-stale', note: t('ai-assistant.entry.review-stale', { defaultValue: 'this proposal changed after it was reviewed in the workspace - review it again before entering.' }) } as any
106
112
  const answer = { ...(await apply(context.sessionId, proposal.key, reviewed)), op: proposal.op }
113
+ if (answer.verdict === 'refused' && answer.errorCode === 'door-closed' && answer.refusal) {
114
+ try {
115
+ await reportRefusal({ level: 'error', message: answer.refusal.message, ex: [answer.refusal] })
116
+ } catch {
117
+ /* Notification delivery is best-effort; it never changes the stored proposal's refusal. */
118
+ }
119
+ }
107
120
  if (answer.errorCode === 'review-stale')
108
121
  return { verdict: 'refused', errorCode: 'review-stale', applied: 0, missed: 0, note: t('ai-assistant.entry.review-stale', { defaultValue: 'this proposal changed after it was reviewed in the workspace - review it again before entering.' }) }
109
122
  return {
@@ -0,0 +1,21 @@
1
+ /*
2
+ * The card that opens an application's list screen with the conditions of a question (design/ai/read-targets.md §5).
3
+ * It reads nothing and runs nothing - the person presses it and the page opens beside the dock.
4
+ */
5
+
6
+ /** An address inside the application - never another origin, never a script. */
7
+ export function isAppAddress(href: unknown): href is string {
8
+ return typeof href === 'string' && !!href.trim() && !/^[a-z][a-z0-9+.-]*:/i.test(href.trim()) && !href.trim().startsWith('//')
9
+ }
10
+
11
+ export function isOpenPageProposal(proposal: any): boolean {
12
+ return proposal?.kind === 'open-page' && isAppAddress(proposal?.href)
13
+ }
14
+
15
+ /** The line under the card's title - the conditions the screen does not take, said, never dropped quietly. */
16
+ export function openPageNote(proposal: any, t: (key: string, options?: any) => string): string {
17
+ const missing: string[] = Array.isArray(proposal?.notAppliedLabels) ? proposal.notAppliedLabels : []
18
+ return missing.length
19
+ ? t('ai-assistant.screen.not-applied', { defaultValue: 'the screen does not take: {conditions}', conditions: missing.join(', ') })
20
+ : ''
21
+ }
@@ -560,6 +560,8 @@ export declare class OxAssistantChat extends LitElement {
560
560
  * 실행 결과는 호스트가 **대화에 기록**한다 — 누가 무엇을 실행했는지가 협의 이력에 남아야 한다.
561
561
  */
562
562
  private renderProposals;
563
+ /** The screen card - which list, the conditions the screen does not take, and the way to open it. */
564
+ private renderOpenPageCard;
563
565
  /** The AI 작업판 card - what it holds, which version, and the way to open it. The person opens it; the model does not. */
564
566
  private renderWorkspaceCard;
565
567
  /**
@@ -54,6 +54,7 @@ import { availableTriggers } from './chat-triggers.js';
54
54
  import './entry-review.js';
55
55
  import { entryGroupOf, entryReadyCount, entryTitle, isChangeProposal, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js';
56
56
  import { isWorkspaceProposal, versionOfCard, workspaceAddress } from '../utils/workspace.js';
57
+ import { isOpenPageProposal, openPageNote } from '../utils/open-page.js';
57
58
  function formatFileSize(bytes) {
58
59
  if (!bytes || bytes < 1024)
59
60
  return `${bytes || 0} B`;
@@ -3868,6 +3869,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3868
3869
  /* A workspace card opens the page; it runs nothing (design/ai/workspace.md §4). */
3869
3870
  if (isWorkspaceProposal(p))
3870
3871
  return this.renderWorkspaceCard(p, key);
3872
+ /* A screen card opens the application's list with the question's conditions; it runs nothing (design/ai/read-targets.md §5). */
3873
+ if (isOpenPageProposal(p))
3874
+ return this.renderOpenPageCard(p);
3871
3875
  const card = proposalCardState(key, { outcomes, requested, sentHere: this.sentProposals.has(key) });
3872
3876
  const entry = isEntryProposal(p);
3873
3877
  const replaced = entry && latestEntry?.get(entryGroupOf(p)) !== key && card.state === 'ready';
@@ -3959,6 +3963,21 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3959
3963
  </div>
3960
3964
  `;
3961
3965
  }
3966
+ /** The screen card - which list, the conditions the screen does not take, and the way to open it. */
3967
+ renderOpenPageCard(p) {
3968
+ const note = openPageNote(p, (key, options) => String(i18next.t(key, options)));
3969
+ const open = () => this.dispatchEvent(new CustomEvent('assistant-open-page', { detail: { href: p.href }, bubbles: true, composed: true }));
3970
+ return html `
3971
+ <div class="proposal workspace">
3972
+ <md-icon>${p.icon || 'open_in_new'}</md-icon>
3973
+ <span class="pbody">
3974
+ <span class="plabel">${p.label}</span>
3975
+ ${note ? html `<span class="preason">${note}</span>` : nothing}
3976
+ </span>
3977
+ <button class="run" @click=${open}>${i18next.t('ai-assistant.screen.open', { defaultValue: 'open the screen' })}</button>
3978
+ </div>
3979
+ `;
3980
+ }
3962
3981
  /** The AI 작업판 card - what it holds, which version, and the way to open it. The person opens it; the model does not. */
3963
3982
  renderWorkspaceCard(p, key) {
3964
3983
  const { version, of } = versionOfCard(this.lines.map(one => one.proposals ?? []), p.workspace.name, key);