@things-factory/ai-assistant 10.1.227 → 10.1.229

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 (75) hide show
  1. package/client/components/assistant-chat.ts +27 -4
  2. package/client/components/assistant-dock.ts +18 -2
  3. package/client/components/proposal-outcome-line.ts +5 -0
  4. package/client/utils/applied-screens.ts +36 -0
  5. package/client/utils/assistant-proposals.ts +2 -0
  6. package/client/utils/assistant-selection.ts +3 -2
  7. package/client/utils/entry-proposal.ts +58 -3
  8. package/dist-client/components/assistant-chat.d.ts +3 -0
  9. package/dist-client/components/assistant-chat.js +27 -4
  10. package/dist-client/components/assistant-chat.js.map +1 -1
  11. package/dist-client/components/assistant-dock.js +18 -2
  12. package/dist-client/components/assistant-dock.js.map +1 -1
  13. package/dist-client/components/proposal-outcome-line.d.ts +2 -0
  14. package/dist-client/components/proposal-outcome-line.js +4 -0
  15. package/dist-client/components/proposal-outcome-line.js.map +1 -1
  16. package/dist-client/tsconfig.tsbuildinfo +1 -1
  17. package/dist-client/utils/applied-screens.d.ts +21 -0
  18. package/dist-client/utils/applied-screens.js +32 -0
  19. package/dist-client/utils/applied-screens.js.map +1 -0
  20. package/dist-client/utils/assistant-proposals.d.ts +2 -0
  21. package/dist-client/utils/assistant-proposals.js.map +1 -1
  22. package/dist-client/utils/assistant-selection.d.ts +1 -1
  23. package/dist-client/utils/assistant-selection.js +3 -2
  24. package/dist-client/utils/assistant-selection.js.map +1 -1
  25. package/dist-client/utils/entry-proposal.d.ts +22 -0
  26. package/dist-client/utils/entry-proposal.js +56 -3
  27. package/dist-client/utils/entry-proposal.js.map +1 -1
  28. package/dist-server/service/assistant-mode/index.d.ts +1 -0
  29. package/dist-server/service/assistant-mode/index.js +5 -1
  30. package/dist-server/service/assistant-mode/index.js.map +1 -1
  31. package/dist-server/service/assistant-mode/record-subject.d.ts +25 -0
  32. package/dist-server/service/assistant-mode/record-subject.js +35 -0
  33. package/dist-server/service/assistant-mode/record-subject.js.map +1 -0
  34. package/dist-server/service/chat-session/attachment-tool-result.d.ts +1 -0
  35. package/dist-server/service/chat-session/attachment-tool-result.js +11 -0
  36. package/dist-server/service/chat-session/attachment-tool-result.js.map +1 -1
  37. package/dist-server/service/chat-session/attachment-tools.js +5 -2
  38. package/dist-server/service/chat-session/attachment-tools.js.map +1 -1
  39. package/dist-server/service/data-entry/entry-screen.d.ts +10 -0
  40. package/dist-server/service/data-entry/entry-screen.js +18 -0
  41. package/dist-server/service/data-entry/entry-screen.js.map +1 -0
  42. package/dist-server/service/data-entry/entry-sources.js +6 -1
  43. package/dist-server/service/data-entry/entry-sources.js.map +1 -1
  44. package/dist-server/service/data-entry/entry-target.d.ts +6 -0
  45. package/dist-server/service/data-entry/entry-target.js.map +1 -1
  46. package/dist-server/service/data-entry/entry-tools.d.ts +3 -0
  47. package/dist-server/service/data-entry/entry-tools.js +3 -0
  48. package/dist-server/service/data-entry/entry-tools.js.map +1 -1
  49. package/dist-server/service/data-entry/entry-update.d.ts +3 -0
  50. package/dist-server/service/data-entry/entry-update.js +3 -0
  51. package/dist-server/service/data-entry/entry-update.js.map +1 -1
  52. package/dist-server/tsconfig.tsbuildinfo +1 -1
  53. package/package.json +9 -9
  54. package/server/service/assistant-mode/index.ts +2 -0
  55. package/server/service/assistant-mode/record-subject.test.ts +41 -0
  56. package/server/service/assistant-mode/record-subject.ts +44 -0
  57. package/server/service/chat-session/attachment-tool-result.test.ts +30 -1
  58. package/server/service/chat-session/attachment-tool-result.ts +12 -0
  59. package/server/service/chat-session/attachment-tools.ts +5 -3
  60. package/server/service/data-entry/data-entry.test.ts +18 -0
  61. package/server/service/data-entry/entry-screen.ts +14 -0
  62. package/server/service/data-entry/entry-sources.ts +5 -1
  63. package/server/service/data-entry/entry-target.ts +6 -0
  64. package/server/service/data-entry/entry-tools.ts +3 -0
  65. package/server/service/data-entry/entry-update.ts +3 -0
  66. package/server/service/data-read/data-read.test.ts +3 -1
  67. package/test/applied-screens.test.ts +65 -0
  68. package/test/assistant-dock.test.ts +6 -1
  69. package/test/entry-proposal.test.ts +46 -0
  70. package/translations/en.json +3 -0
  71. package/translations/es.json +3 -0
  72. package/translations/ja.json +3 -0
  73. package/translations/ko.json +3 -0
  74. package/translations/ms.json +3 -0
  75. package/translations/zh.json +3 -0
@@ -1,6 +1,6 @@
1
1
  import { replyDestination, quotedReply } from './reply-message.js'
2
2
  import { rosterName } from './roster-identity'
3
- import { notify } from '@operato/layout'
3
+ import { entity360KindRegistered, notify } from '@operato/layout'
4
4
  import './assistant-invite.js'
5
5
  import { TypingRoom } from '../utils/assistant-typing.js'
6
6
  import { isInviteProposal, personLine, type InvitePerson } from '../utils/invite-proposal.js'
@@ -78,11 +78,11 @@ import {
78
78
  */
79
79
  import { chatDefaults } from './chat-defaults.js'
80
80
  import { isDifferentConversation, resolveConversation, type AssistantMode, type AssistantModeView } from './assistant-mode.js'
81
- import { parseProposalOutcomeLine, parseProposalRequestLine, proposalCardState, type ParsedProposalOutcome } from './proposal-outcome-line.js'
81
+ import { entryWentIn, parseProposalOutcomeLine, parseProposalRequestLine, proposalCardState, type ParsedProposalOutcome } from './proposal-outcome-line.js'
82
82
  import './entry-review.js'
83
- import { entryGroupOf, entryReadyCount, entryTitle, isChangeProposal, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js'
83
+ import { entryGroupOf, entryReadyCount, entryRecordsOf, entryScreenAddress, entryTitle, isChangeProposal, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js'
84
84
  import { isWorkspaceProposal, versionOfCard, workspaceAddress } from '../utils/workspace.js'
85
- import { isOpenPageProposal, openPageNote } from '../utils/open-page.js'
85
+ import { isAppAddress, isOpenPageProposal, openPageNote } from '../utils/open-page.js'
86
86
  import { proposalViewOf } from '../utils/assistant-proposals.js'
87
87
 
88
88
  interface ChatLine {
@@ -4559,6 +4559,7 @@ export class OxAssistantChat extends LitElement {
4559
4559
  /* The card's state is the conversation's rows - read once for every card of this line. */
4560
4560
  const outcomes = this.proposalOutcomes
4561
4561
  const requested = this.proposalRequests
4562
+ const records = this.entryRecords
4562
4563
  /* A data entry card replaced by a newer one for the same record cannot be entered (design/ai/data-entry.md §5). */
4563
4564
  const latestEntry = items.some(isEntryProposal) ? latestEntryKeys(this.lines.map(one => one.proposals ?? [])) : undefined
4564
4565
  return html`
@@ -4626,6 +4627,12 @@ export class OxAssistantChat extends LitElement {
4626
4627
  : i18next.t('ai-assistant.button.run-proposal', { defaultValue: 'run' })}
4627
4628
  </button>
4628
4629
  `}
4630
+ ${/* What went in is seen on its screen - the card's next step once applied, beside its outcome. */ ''}
4631
+ ${entry && entryWentIn(card) && isAppAddress(p.screen)
4632
+ ? html`<button class="run see" @click=${() => this.openEntryScreen(p, key, records)}>
4633
+ ${i18next.t('ai-assistant.entry.see-on-screen', { defaultValue: 'see on the screen' })}
4634
+ </button>`
4635
+ : nothing}
4629
4636
  ${/*
4630
4637
  Drawn only where the host has said it staged this candidate, in the host's own
4631
4638
  words. Not knowing is not a claim: with no word from the host, nothing is drawn.
@@ -4953,6 +4960,22 @@ export class OxAssistantChat extends LitElement {
4953
4960
  )
4954
4961
  }
4955
4962
 
4963
+ /** The records each entry card entered - from the conversation's notes, like the outcomes, so a refresh keeps them. */
4964
+ private get entryRecords(): Map<string, string[]> {
4965
+ const records = new Map<string, string[]>()
4966
+ for (const line of this.lines) {
4967
+ if (line.role !== 'system') continue
4968
+ const entered = entryRecordsOf(line.content ?? '')
4969
+ if (entered) records.set(entered.key, entered.ids)
4970
+ }
4971
+ return records
4972
+ }
4973
+
4974
+ private openEntryScreen(p: any, key: string, records: Map<string, string[]>) {
4975
+ const href = entryScreenAddress(p, records.get(key), entity360KindRegistered)
4976
+ this.dispatchEvent(new CustomEvent('assistant-open-page', { detail: { href }, bubbles: true, composed: true }))
4977
+ }
4978
+
4956
4979
  /** Keys whose run was requested - from the conversation's rows, like the outcomes. */
4957
4980
  private get proposalRequests(): Set<string> {
4958
4981
  const requested = new Set<string>()
@@ -11,7 +11,7 @@ import { keyed } from 'lit/directives/keyed.js'
11
11
 
12
12
  import { client } from '@operato/graphql'
13
13
  import { i18next, localize } from '@operato/i18n'
14
- import { closeOverlay, notify } from '@operato/layout'
14
+ import { closeOverlay, notify, refreshEntity360 } from '@operato/layout'
15
15
  import { OxPrompt } from '@operato/popup'
16
16
  import { AppController, navigate, store } from '@operato/shell'
17
17
  import { ScrollbarStyles } from '@operato/styles'
@@ -31,6 +31,7 @@ import {
31
31
  type AssistantSurface
32
32
  } from '../utils/assistant-surface.js'
33
33
  import { runProposal, setEntryProposalHandler, type ProposalContext, type ProposalOutcome } from '../utils/assistant-proposals.js'
34
+ import { refreshAppliedScreens, type AppliedScreensDeps } from '../utils/applied-screens.js'
34
35
  /* The invite card's handler - ai-assistant's own tool, registered by its own module (design/ai/shared-dock.md §4). */
35
36
  import '../utils/invite-proposal-register.js'
36
37
  import { entryProposalHandler, type EntryApplyAnswer } from '../utils/entry-proposal.js'
@@ -62,6 +63,15 @@ setEntryProposalHandler(
62
63
  )
63
64
  )
64
65
 
66
+ /* The page on screen and the shown 360 - what an applied card's records may be shown on. */
67
+ const APPLIED_SCREENS: AppliedScreensDeps = {
68
+ page: () => {
69
+ const route = (store.getState() as any)?.route ?? {}
70
+ return { name: route.page, element: route.activePage }
71
+ },
72
+ refresh360: refreshEntity360
73
+ }
74
+
65
75
  const MODE_QUERY = gql`
66
76
  query ($subject: String!) {
67
77
  assistantModeFor(subject: $subject) {
@@ -785,9 +795,15 @@ export class AssistantDock extends localize(i18next)(LitElement) {
785
795
  if (outcome.verdict === 'unavailable' && outcome.errorCode === 'unknown-proposal-kind') {
786
796
  notify({ level: 'error', message: t('proposal-not-runnable', 'this proposal cannot be run on this screen.') })
787
797
  }
788
- await this.report(sessionId, proposalKey, outcome)
798
+ const { records, ...reported } = outcome
799
+ await this.report(sessionId, proposalKey, reported)
789
800
  /* What the handler did, in the person's words, stays in the conversation - what was created, why it did not run. */
790
801
  if (outcome.note && sessionId) await this.appendNote(sessionId, outcome.note)
802
+ /* The list or 360 behind the dock shows what went in, without the person refreshing it. */
803
+ if (outcome.verdict === 'accepted' && (outcome.applied ?? 1) > 0)
804
+ void refreshAppliedScreens(proposal, records, APPLIED_SCREENS).then(kept => {
805
+ if (kept) notify({ level: 'info', message: t('screen-kept-edits', 'the list was not read again - it has unsaved edits. save them, then refresh it to see what went in.') })
806
+ })
791
807
  }
792
808
 
793
809
  /**
@@ -67,3 +67,8 @@ export function proposalCardState(
67
67
  if (seen.requested.has(key)) return { state: 'unconfirmed' }
68
68
  return { state: 'ready' }
69
69
  }
70
+
71
+ /** Something went in - the card then offers the screen where it is seen. A refusal, or nothing entered, offers none. */
72
+ export function entryWentIn(card: ProposalCardState): boolean {
73
+ return card.state === 'answered' && card.outcome.verdict === 'accepted' && (card.outcome.applied ?? 0) > 0
74
+ }
@@ -0,0 +1,36 @@
1
+ /*
2
+ * The screens that show what an applied card wrote read it again (2026-10-29). Applied from the dock, a card left the
3
+ * list and the 360 behind it as they were, and people took it for not applied.
4
+ *
5
+ * Only a screen that shows those records reads again - the shown 360 of the record or of a record the card is about,
6
+ * and the page that is the card's screen. Another page keeps its rows and its scroll. A page with unsaved edits is
7
+ * not asked about them by a refresh nobody pressed: it is left as it is, and its own refresh control reads it later.
8
+ */
9
+
10
+ export interface AppliedScreensDeps {
11
+ /** The page on screen - its route name and the shell's page element (`PageView.refresh`, ADR-0174). */
12
+ page(): { name?: string; element?: { refresh?: (options?: { ask?: boolean }) => Promise<boolean> } }
13
+ /** The 360's refresh, asked whether it is about the shown subject. */
14
+ refresh360(about: (subject: { kind: string; id: string }) => boolean): boolean
15
+ }
16
+
17
+ /**
18
+ * Reads again what shows the records of an applied card. Never throws - a screen that cannot read keeps what it shows.
19
+ * `kept-edits` when the card's page was left unread for its unsaved edits - the person is told, or the list looks unapplied.
20
+ */
21
+ export async function refreshAppliedScreens(proposal: any, ids: string[] | undefined, deps: AppliedScreensDeps): Promise<'kept-edits' | undefined> {
22
+ const kind = String(proposal?.target ?? '').split('.')[1] ?? ''
23
+ const axes: string[] = Array.isArray(proposal?.axes) ? proposal.axes : []
24
+ try {
25
+ deps.refresh360(subject => (subject.kind === kind && !!ids?.includes(subject.id)) || axes.includes(subject.kind))
26
+ } catch {
27
+ /* the 360 keeps what it shows; its refresh button reads it */
28
+ }
29
+ const page = deps.page()
30
+ if (!proposal?.screen || page.name !== proposal.screen || typeof page.element?.refresh !== 'function') return
31
+ try {
32
+ return (await page.element.refresh({ ask: false })) === false ? 'kept-edits' : undefined
33
+ } catch {
34
+ /* the page keeps what it shows; its refresh control reads it */
35
+ }
36
+ }
@@ -14,6 +14,8 @@ export interface ProposalOutcome {
14
14
  missed?: number
15
15
  /** A line for the conversation, besides the outcome itself (e.g. what was created). */
16
16
  note?: string
17
+ /** The ids of the records it wrote - the screens that show them read again. Not reported to the conversation. */
18
+ records?: string[]
17
19
  }
18
20
 
19
21
  export interface ProposalContext {
@@ -6,6 +6,7 @@
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
+ import { ENTRY_RECORDS_PREFIX } from './entry-proposal.js'
9
10
 
10
11
  /**
11
12
  * The grid's own row picker - a list that hands its rows to the assistant shows it beside `ox-assistant-selection-bar`
@@ -64,11 +65,11 @@ export function selectionNote(selection: AssistantSelection, t: SelectionTransla
64
65
  return `${named}\n${keys}`
65
66
  }
66
67
 
67
- /** A note as the conversation draws it - without the line only the model reads. */
68
+ /** A note as the conversation draws it - without the lines only the model reads (a selection's keys, an entry's records). */
68
69
  export function noteForDisplay(content: string): string {
69
70
  return (content ?? '')
70
71
  .split('\n')
71
- .filter(line => !line.startsWith(SELECTION_KEYS_PREFIX))
72
+ .filter(line => !line.startsWith(SELECTION_KEYS_PREFIX) && !line.startsWith(ENTRY_RECORDS_PREFIX))
72
73
  .join('\n')
73
74
  .trim()
74
75
  }
@@ -5,6 +5,8 @@
5
5
  * again - the newer card replaces the older one. Pressing asks the server to enter the stored rows; the
6
6
  * screen sends no rows of its own.
7
7
  */
8
+ import { encodeListQuery } from '@operato/utils/list-query.js'
9
+
8
10
  import type { ProposalContext, ProposalOutcome } from './assistant-proposals.js'
9
11
  import { reviewedHashOf, reviewStillHolds } from './entry-workspace.js'
10
12
 
@@ -49,6 +51,9 @@ export interface EntryApplyAnswer extends ProposalOutcome {
49
51
  refusal?: { message: string; extensions: Record<string, unknown>; path: string[] }
50
52
  /** Set by the handler from the card - which words the conversation line uses. */
51
53
  op?: string
54
+ /** Set by the handler from the card - which card and record kind the entered ids belong to. */
55
+ key?: string
56
+ target?: string
52
57
  }
53
58
 
54
59
  export function isEntryProposal(proposal: any): boolean {
@@ -90,7 +95,56 @@ export function entryOutcomeNote(answer: EntryApplyAnswer, t: (key: string, opti
90
95
  : t('ai-assistant.entry.entered-none', { defaultValue: 'no row was entered.' })
91
96
  const lines = refused.slice(0, 20).map(row => t('ai-assistant.entry.row-refused', { defaultValue: 'row {n}: {reason}', n: row.index + 1, reason: row.error || '' }))
92
97
  const more = refused.length > 20 ? [t('ai-assistant.entry.more-refused', { defaultValue: '{count} more rows were not entered.', count: refused.length - 20 })] : []
93
- return [head, ...lines, ...more].join('\n')
98
+ const records = entryRecordsLine(answer)
99
+ return [head, ...appliedNames(answer, t), ...lines, ...more, ...(records ? [records] : [])].join('\n')
100
+ }
101
+
102
+ /**
103
+ * The line of an entry note that names the records a card entered - for the card, which opens them on their screen, and
104
+ * for the model. The conversation does not draw it. Not `[keys]`: that line is the person's selection, and a card's
105
+ * records must not replace what "these" means.
106
+ */
107
+ export const ENTRY_RECORDS_PREFIX = '[records]'
108
+
109
+ const enteredIds = (answer: EntryApplyAnswer): string[] => [...new Set((answer.rows ?? []).filter(row => row.ok && row.id).map(row => row.id as string))]
110
+
111
+ function entryRecordsLine(answer: EntryApplyAnswer): string {
112
+ const ids = enteredIds(answer)
113
+ return ids.length && answer.key && answer.target ? `${ENTRY_RECORDS_PREFIX} ${answer.key} ${answer.target} ${ids.join(' ')}` : ''
114
+ }
115
+
116
+ /** The card key, record kind and ids of an entry note's records line - undefined for any other content. */
117
+ export function entryRecordsOf(content: string): { key: string; target: string; ids: string[] } | undefined {
118
+ const line = (content ?? '').split('\n').find(one => one.startsWith(`${ENTRY_RECORDS_PREFIX} `))
119
+ const [key, target, ...ids] = line ? line.slice(ENTRY_RECORDS_PREFIX.length).trim().split(/\s+/) : []
120
+ return key && target && ids.length ? { key, target, ids } : undefined
121
+ }
122
+
123
+ /** At most this many ids narrow a list in its address - more open the list as it is. */
124
+ export const ENTRY_SCREEN_IDS = 50
125
+
126
+ /**
127
+ * Where the card's "see on the screen" goes. One record of a kind with a 360 opens that 360 beside its list; records
128
+ * of the list the screen shows narrow it to them, a condition the person sees and removes; otherwise the screen as it is.
129
+ */
130
+ export function entryScreenAddress(proposal: any, ids: string[] | undefined, has360: (kind: string) => boolean): string {
131
+ const screen: string = proposal?.screen
132
+ const kind = String(proposal?.target ?? '').split('.')[1] ?? ''
133
+ if (ids?.length === 1 && kind && has360(kind)) return `${screen}?${new URLSearchParams({ f: `${kind}:${ids[0]}` })}`
134
+ if (ids?.length && ids.length <= ENTRY_SCREEN_IDS && proposal?.screenLists)
135
+ return `${screen}?${encodeListQuery({ filters: [{ name: 'id', operator: 'in', value: ids }] })}`
136
+ return screen
137
+ }
138
+
139
+ /*
140
+ * Which records went in, by name - the person finds them on the screen by it, and the model reads which ones they were.
141
+ * Lines of one record share its name; a record without one is only counted.
142
+ */
143
+ function appliedNames(answer: EntryApplyAnswer, t: (key: string, options?: any) => string): string[] {
144
+ const names = [...new Set((answer.rows ?? []).filter(row => row.ok && row.label).map(row => row.label as string))]
145
+ if (!names.length) return []
146
+ const shown = names.slice(0, 10).join(', ')
147
+ return [names.length > 10 ? `${shown} ${t('ai-assistant.entry.more-applied', { defaultValue: 'and {count} more', count: names.length - 10 })}` : shown]
94
148
  }
95
149
 
96
150
  /** The handler every entry card runs - built with the mutation so the rules are tested without a server. */
@@ -109,7 +163,7 @@ export function entryProposalHandler(
109
163
  /* Reviewed in the 작업 패널: entered only as it was shown there (design/ai/workspace.md §6-1) - here, and again at the server. */
110
164
  const reviewed = reviewedOf(proposal.key)
111
165
  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
112
- const answer = { ...(await apply(context.sessionId, proposal.key, reviewed)), op: proposal.op }
166
+ const answer = { ...(await apply(context.sessionId, proposal.key, reviewed)), op: proposal.op, key: proposal.key, target: proposal.target }
113
167
  if (answer.verdict === 'refused' && answer.errorCode === 'door-closed' && answer.refusal) {
114
168
  try {
115
169
  await reportRefusal({ level: 'error', message: answer.refusal.message, ex: [answer.refusal] })
@@ -124,7 +178,8 @@ export function entryProposalHandler(
124
178
  ...(answer.errorCode ? { errorCode: answer.errorCode } : {}),
125
179
  applied: answer.applied ?? 0,
126
180
  missed: answer.missed ?? 0,
127
- note: entryOutcomeNote(answer, t)
181
+ note: entryOutcomeNote(answer, t),
182
+ records: enteredIds(answer)
128
183
  }
129
184
  }
130
185
  }
@@ -619,6 +619,9 @@ export declare class OxAssistantChat extends LitElement {
619
619
  * (`AssistantSurface.proposalOutcome`), never to run it again. The dock reports what it learns.
620
620
  */
621
621
  private checkProposal;
622
+ /** The records each entry card entered - from the conversation's notes, like the outcomes, so a refresh keeps them. */
623
+ private get entryRecords();
624
+ private openEntryScreen;
622
625
  /** Keys whose run was requested - from the conversation's rows, like the outcomes. */
623
626
  private get proposalRequests();
624
627
  /**
@@ -2,7 +2,7 @@ var OxAssistantChat_1;
2
2
  import { __decorate, __metadata } from "tslib";
3
3
  import { replyDestination, quotedReply } from './reply-message.js';
4
4
  import { rosterName } from './roster-identity';
5
- import { notify } from '@operato/layout';
5
+ import { entity360KindRegistered, notify } from '@operato/layout';
6
6
  import './assistant-invite.js';
7
7
  import { TypingRoom } from '../utils/assistant-typing.js';
8
8
  import { isInviteProposal, personLine } from '../utils/invite-proposal.js';
@@ -62,11 +62,11 @@ import { buildMentionCandidates, buildCatalogCandidates, buildSlashCandidates, b
62
62
  */
63
63
  import { chatDefaults } from './chat-defaults.js';
64
64
  import { isDifferentConversation, resolveConversation } from './assistant-mode.js';
65
- import { parseProposalOutcomeLine, parseProposalRequestLine, proposalCardState } from './proposal-outcome-line.js';
65
+ import { entryWentIn, parseProposalOutcomeLine, parseProposalRequestLine, proposalCardState } from './proposal-outcome-line.js';
66
66
  import './entry-review.js';
67
- import { entryGroupOf, entryReadyCount, entryTitle, isChangeProposal, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js';
67
+ import { entryGroupOf, entryReadyCount, entryRecordsOf, entryScreenAddress, entryTitle, isChangeProposal, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js';
68
68
  import { isWorkspaceProposal, versionOfCard, workspaceAddress } from '../utils/workspace.js';
69
- import { isOpenPageProposal, openPageNote } from '../utils/open-page.js';
69
+ import { isAppAddress, isOpenPageProposal, openPageNote } from '../utils/open-page.js';
70
70
  import { proposalViewOf } from '../utils/assistant-proposals.js';
71
71
  function formatFileSize(bytes) {
72
72
  if (!bytes || bytes < 1024)
@@ -4117,6 +4117,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4117
4117
  /* The card's state is the conversation's rows - read once for every card of this line. */
4118
4118
  const outcomes = this.proposalOutcomes;
4119
4119
  const requested = this.proposalRequests;
4120
+ const records = this.entryRecords;
4120
4121
  /* A data entry card replaced by a newer one for the same record cannot be entered (design/ai/data-entry.md §5). */
4121
4122
  const latestEntry = items.some(isEntryProposal) ? latestEntryKeys(this.lines.map(one => one.proposals ?? [])) : undefined;
4122
4123
  return html `
@@ -4186,6 +4187,12 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4186
4187
  : i18next.t('ai-assistant.button.run-proposal', { defaultValue: 'run' })}
4187
4188
  </button>
4188
4189
  `}
4190
+ ${ /* What went in is seen on its screen - the card's next step once applied, beside its outcome. */''}
4191
+ ${entry && entryWentIn(card) && isAppAddress(p.screen)
4192
+ ? html `<button class="run see" @click=${() => this.openEntryScreen(p, key, records)}>
4193
+ ${i18next.t('ai-assistant.entry.see-on-screen', { defaultValue: 'see on the screen' })}
4194
+ </button>`
4195
+ : nothing}
4189
4196
  ${ /*
4190
4197
  Drawn only where the host has said it staged this candidate, in the host's own
4191
4198
  words. Not knowing is not a claim: with no word from the host, nothing is drawn.
@@ -4469,6 +4476,22 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
4469
4476
  composed: true
4470
4477
  }));
4471
4478
  }
4479
+ /** The records each entry card entered - from the conversation's notes, like the outcomes, so a refresh keeps them. */
4480
+ get entryRecords() {
4481
+ const records = new Map();
4482
+ for (const line of this.lines) {
4483
+ if (line.role !== 'system')
4484
+ continue;
4485
+ const entered = entryRecordsOf(line.content ?? '');
4486
+ if (entered)
4487
+ records.set(entered.key, entered.ids);
4488
+ }
4489
+ return records;
4490
+ }
4491
+ openEntryScreen(p, key, records) {
4492
+ const href = entryScreenAddress(p, records.get(key), entity360KindRegistered);
4493
+ this.dispatchEvent(new CustomEvent('assistant-open-page', { detail: { href }, bubbles: true, composed: true }));
4494
+ }
4472
4495
  /** Keys whose run was requested - from the conversation's rows, like the outcomes. */
4473
4496
  get proposalRequests() {
4474
4497
  const requested = new Set();