@things-factory/ai-assistant 10.1.210 → 10.1.213
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/components/assistant-chat.ts +115 -2
- package/client/components/assistant-dock.ts +106 -3
- package/client/components/workspace-view.ts +56 -3
- package/client/index.ts +3 -0
- package/client/utils/assistant-ask.test.ts +38 -0
- package/client/utils/assistant-ask.ts +35 -1
- package/client/utils/assistant-proposals.ts +23 -0
- package/client/utils/assistant-selection.test.ts +30 -0
- package/client/utils/assistant-selection.ts +47 -0
- package/client/utils/assistant-session-controller.ts +2 -0
- package/client/utils/assistant-session-transport.ts +1 -1
- package/client/utils/invite-proposal-register.ts +31 -0
- package/client/utils/invite-proposal.test.ts +40 -0
- package/client/utils/invite-proposal.ts +54 -0
- package/dist-client/components/assistant-chat.d.ts +18 -0
- package/dist-client/components/assistant-chat.js +117 -2
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.d.ts +11 -0
- package/dist-client/components/assistant-dock.js +97 -4
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/workspace-view.d.ts +2 -0
- package/dist-client/components/workspace-view.js +58 -3
- package/dist-client/components/workspace-view.js.map +1 -1
- package/dist-client/index.d.ts +2 -0
- package/dist-client/index.js +3 -0
- package/dist-client/index.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/assistant-ask.d.ts +22 -1
- package/dist-client/utils/assistant-ask.js +19 -0
- package/dist-client/utils/assistant-ask.js.map +1 -1
- package/dist-client/utils/assistant-ask.test.d.ts +1 -0
- package/dist-client/utils/assistant-ask.test.js +40 -0
- package/dist-client/utils/assistant-ask.test.js.map +1 -0
- package/dist-client/utils/assistant-proposals.d.ts +14 -0
- package/dist-client/utils/assistant-proposals.js +7 -0
- package/dist-client/utils/assistant-proposals.js.map +1 -1
- package/dist-client/utils/assistant-selection.d.ts +19 -0
- package/dist-client/utils/assistant-selection.js +35 -0
- package/dist-client/utils/assistant-selection.js.map +1 -0
- package/dist-client/utils/assistant-selection.test.d.ts +1 -0
- package/dist-client/utils/assistant-selection.test.js +26 -0
- package/dist-client/utils/assistant-selection.test.js.map +1 -0
- package/dist-client/utils/assistant-session-controller.d.ts +2 -0
- package/dist-client/utils/assistant-session-controller.js.map +1 -1
- package/dist-client/utils/assistant-session-transport.js +1 -1
- package/dist-client/utils/assistant-session-transport.js.map +1 -1
- package/dist-client/utils/invite-proposal-register.d.ts +1 -0
- package/dist-client/utils/invite-proposal-register.js +24 -0
- package/dist-client/utils/invite-proposal-register.js.map +1 -0
- package/dist-client/utils/invite-proposal.d.ts +17 -0
- package/dist-client/utils/invite-proposal.js +37 -0
- package/dist-client/utils/invite-proposal.js.map +1 -0
- package/dist-client/utils/invite-proposal.test.d.ts +1 -0
- package/dist-client/utils/invite-proposal.test.js +35 -0
- package/dist-client/utils/invite-proposal.test.js.map +1 -0
- package/dist-server/service/assistant-chat-resolver.js +2 -0
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/index.d.ts +2 -1
- package/dist-server/service/assistant-mode/index.js +6 -1
- package/dist-server/service/assistant-mode/index.js.map +1 -1
- package/dist-server/service/assistant-mode/mode-categories.d.ts +14 -1
- package/dist-server/service/assistant-mode/mode-categories.js +29 -4
- package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
- package/dist-server/service/assistant-mode/screen-rules.d.ts +6 -0
- package/dist-server/service/assistant-mode/screen-rules.js +21 -6
- package/dist-server/service/assistant-mode/screen-rules.js.map +1 -1
- package/dist-server/service/data-read/read-target.d.ts +24 -3
- package/dist-server/service/data-read/read-target.js +42 -10
- package/dist-server/service/data-read/read-target.js.map +1 -1
- package/dist-server/service/data-read/read-tools.d.ts +18 -1
- package/dist-server/service/data-read/read-tools.js +19 -5
- package/dist-server/service/data-read/read-tools.js.map +1 -1
- package/dist-server/service/index.d.ts +5 -1
- package/dist-server/service/index.js +10 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/people/index.d.ts +4 -0
- package/dist-server/service/people/index.js +22 -0
- package/dist-server/service/people/index.js.map +1 -0
- package/dist-server/service/people/invite-tools.d.ts +34 -0
- package/dist-server/service/people/invite-tools.js +144 -0
- package/dist-server/service/people/invite-tools.js.map +1 -0
- package/dist-server/service/people/people-keys.d.ts +8 -0
- package/dist-server/service/people/people-keys.js +16 -0
- package/dist-server/service/people/people-keys.js.map +1 -0
- package/dist-server/service/people/people-read.d.ts +4 -0
- package/dist-server/service/people/people-read.js +57 -0
- package/dist-server/service/people/people-read.js.map +1 -0
- package/dist-server/service/testing/mode-conformance.js +3 -0
- package/dist-server/service/testing/mode-conformance.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/server/service/assistant-chat-resolver.ts +2 -0
- package/server/service/assistant-mode/conversation-category.test.ts +35 -0
- package/server/service/assistant-mode/index.ts +3 -1
- package/server/service/assistant-mode/mode-categories.ts +41 -4
- package/server/service/assistant-mode/mode-front.test.ts +5 -3
- package/server/service/assistant-mode/resolve.test.ts +1 -1
- package/server/service/assistant-mode/screen-rules.test.ts +23 -1
- package/server/service/assistant-mode/screen-rules.ts +22 -5
- package/server/service/data-entry/data-entry.test.ts +2 -2
- package/server/service/data-read/detail-lists.test.ts +61 -0
- package/server/service/data-read/read-target.ts +54 -9
- package/server/service/data-read/read-tools.ts +32 -6
- package/server/service/index.ts +7 -1
- package/server/service/people/index.ts +13 -0
- package/server/service/people/invite-tools.ts +159 -0
- package/server/service/people/invite.test.ts +159 -0
- package/server/service/people/people-keys.ts +16 -0
- package/server/service/people/people-read.test.ts +82 -0
- package/server/service/people/people-read.ts +56 -0
- package/server/service/testing/mode-conformance.test.ts +14 -1
- package/server/service/testing/mode-conformance.ts +2 -0
- package/test/assistant-dock.test.ts +73 -0
- package/translations/en.json +18 -1
- package/translations/es.json +18 -1
- package/translations/ja.json +18 -1
- package/translations/ko.json +19 -2
- package/translations/ms.json +18 -1
- package/translations/zh.json +18 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AssistantSelection } from './assistant-selection.js';
|
|
1
2
|
export interface AssistantAsk {
|
|
2
3
|
/** Sent as the next question. */
|
|
3
4
|
ask?: string;
|
|
@@ -14,9 +15,29 @@ export interface AssistantAsk {
|
|
|
14
15
|
}[];
|
|
15
16
|
/** A note stored first: what the screen was showing when the person asked. */
|
|
16
17
|
context?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The records the person picked on the screen - a screen request (「AI와 작업」, design/ai/shared-dock.md §2-11-1). It
|
|
20
|
+
* goes to this screen's conversation, never a new one; the selection is noted for that turn (a line the person reads,
|
|
21
|
+
* the ids for the model - assistant-selection.ts) and `ask` is sent at once.
|
|
22
|
+
*/
|
|
23
|
+
selection?: AssistantSelection;
|
|
24
|
+
}
|
|
25
|
+
/** What became of a request - a screen keeps what the person wrote when it was not sent, and says nothing twice. */
|
|
26
|
+
export interface AssistantAskOutcome {
|
|
27
|
+
sent: boolean;
|
|
28
|
+
/** Why it was not sent, in the person's words - the dock has already said it. */
|
|
29
|
+
reason?: string;
|
|
17
30
|
}
|
|
18
31
|
type Listener = () => void;
|
|
19
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Hands the request to the dock. The promise says what became of it once the dock has dealt with it - sent, or not and
|
|
34
|
+
* why. A request replaced before the dock took it is not sent, and says so.
|
|
35
|
+
*/
|
|
36
|
+
export declare function askAssistant(ask: AssistantAsk): Promise<AssistantAskOutcome>;
|
|
37
|
+
/** The dock tells the screen what became of its request. Once; later calls do nothing. */
|
|
38
|
+
export declare function settleAssistantAsk(ask: AssistantAsk, outcome: AssistantAskOutcome): void;
|
|
39
|
+
/** The waiting request, left waiting - the dock decides how to take it before taking it. */
|
|
40
|
+
export declare function peekAssistantAsk(): AssistantAsk | undefined;
|
|
20
41
|
/**
|
|
21
42
|
* The dock could not take it yet and keeps it for when a conversation stands - a draft only. Nothing is announced:
|
|
22
43
|
* it was already asked for, and announcing would make the dock take it again at once.
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
let pending;
|
|
2
2
|
const listeners = new Set();
|
|
3
|
+
const settlers = new WeakMap();
|
|
4
|
+
/**
|
|
5
|
+
* Hands the request to the dock. The promise says what became of it once the dock has dealt with it - sent, or not and
|
|
6
|
+
* why. A request replaced before the dock took it is not sent, and says so.
|
|
7
|
+
*/
|
|
3
8
|
export function askAssistant(ask) {
|
|
9
|
+
if (pending && pending !== ask)
|
|
10
|
+
settleAssistantAsk(pending, { sent: false, reason: 'replaced' });
|
|
11
|
+
const outcome = new Promise(resolve => settlers.set(ask, resolve));
|
|
4
12
|
pending = ask;
|
|
5
13
|
listeners.forEach(listener => listener());
|
|
6
14
|
window.dispatchEvent(new CustomEvent('assistant-dock-open'));
|
|
15
|
+
return outcome;
|
|
16
|
+
}
|
|
17
|
+
/** The dock tells the screen what became of its request. Once; later calls do nothing. */
|
|
18
|
+
export function settleAssistantAsk(ask, outcome) {
|
|
19
|
+
const settle = settlers.get(ask);
|
|
20
|
+
settlers.delete(ask);
|
|
21
|
+
settle?.(outcome);
|
|
22
|
+
}
|
|
23
|
+
/** The waiting request, left waiting - the dock decides how to take it before taking it. */
|
|
24
|
+
export function peekAssistantAsk() {
|
|
25
|
+
return pending;
|
|
7
26
|
}
|
|
8
27
|
/**
|
|
9
28
|
* The dock could not take it yet and keeps it for when a conversation stands - a draft only. Nothing is announced:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-ask.js","sourceRoot":"","sources":["../../client/utils/assistant-ask.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"assistant-ask.js","sourceRoot":"","sources":["../../client/utils/assistant-ask.ts"],"names":[],"mappings":"AAsCA,IAAI,OAAiC,CAAA;AACrC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAY,CAAA;AACrC,MAAM,QAAQ,GAAG,IAAI,OAAO,EAAwD,CAAA;AAEpF;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,GAAiB;IAC5C,IAAI,OAAO,IAAI,OAAO,KAAK,GAAG;QAAE,kBAAkB,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAA;IAChG,MAAM,OAAO,GAAG,IAAI,OAAO,CAAsB,OAAO,CAAC,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAA;IACvF,OAAO,GAAG,GAAG,CAAA;IACb,SAAS,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,CAAC,CAAA;IACzC,MAAM,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,qBAAqB,CAAC,CAAC,CAAA;IAC5D,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,kBAAkB,CAAC,GAAiB,EAAE,OAA4B;IAChF,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAChC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACpB,MAAM,EAAE,CAAC,OAAO,CAAC,CAAA;AACnB,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,gBAAgB;IAC9B,OAAO,OAAO,CAAA;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAiB;IAChD,OAAO,GAAG,GAAG,CAAA;AACf,CAAC;AAED,uCAAuC;AACvC,MAAM,UAAU,gBAAgB;IAC9B,MAAM,GAAG,GAAG,OAAO,CAAA;IACnB,OAAO,GAAG,SAAS,CAAA;IACnB,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,QAAkB;IACtD,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;AACzC,CAAC","sourcesContent":["/*\n * Asking the assistant from another screen - `askAssistant()` (design/ai/shared-dock.md §2-11).\n *\n * Generalised from operato-twin's promotion channel and operato-plant's ask box: a toast's \"ask AI\", a\n * header ask box, a performance page carrying its context. The request waits here until the dock takes it,\n * so a dock that is closed (not mounted) still gets it when it opens.\n */\nimport type { AssistantSelection } from './assistant-selection.js'\nexport interface AssistantAsk {\n /** Sent as the next question. */\n ask?: string\n /** Put in the input, not sent. */\n draft?: string\n /** Ask in the open conversation instead of a new one. */\n continueCurrent?: boolean\n /** A new conversation's name. */\n name?: string\n /** Turns carried into the new conversation before the question. */\n transcript?: { role: 'user' | 'ai'; content: string }[]\n /** A note stored first: what the screen was showing when the person asked. */\n context?: string\n /**\n * The records the person picked on the screen - a screen request (「AI와 작업」, design/ai/shared-dock.md §2-11-1). It\n * goes to this screen's conversation, never a new one; the selection is noted for that turn (a line the person reads,\n * the ids for the model - assistant-selection.ts) and `ask` is sent at once.\n */\n selection?: AssistantSelection\n}\n\n/** What became of a request - a screen keeps what the person wrote when it was not sent, and says nothing twice. */\nexport interface AssistantAskOutcome {\n sent: boolean\n /** Why it was not sent, in the person's words - the dock has already said it. */\n reason?: string\n}\n\ntype Listener = () => void\n\nlet pending: AssistantAsk | undefined\nconst listeners = new Set<Listener>()\nconst settlers = new WeakMap<AssistantAsk, (outcome: AssistantAskOutcome) => void>()\n\n/**\n * Hands the request to the dock. The promise says what became of it once the dock has dealt with it - sent, or not and\n * why. A request replaced before the dock took it is not sent, and says so.\n */\nexport function askAssistant(ask: AssistantAsk): Promise<AssistantAskOutcome> {\n if (pending && pending !== ask) settleAssistantAsk(pending, { sent: false, reason: 'replaced' })\n const outcome = new Promise<AssistantAskOutcome>(resolve => settlers.set(ask, resolve))\n pending = ask\n listeners.forEach(listener => listener())\n window.dispatchEvent(new CustomEvent('assistant-dock-open'))\n return outcome\n}\n\n/** The dock tells the screen what became of its request. Once; later calls do nothing. */\nexport function settleAssistantAsk(ask: AssistantAsk, outcome: AssistantAskOutcome): void {\n const settle = settlers.get(ask)\n settlers.delete(ask)\n settle?.(outcome)\n}\n\n/** The waiting request, left waiting - the dock decides how to take it before taking it. */\nexport function peekAssistantAsk(): AssistantAsk | undefined {\n return pending\n}\n\n/**\n * The dock could not take it yet and keeps it for when a conversation stands - a draft only. Nothing is announced:\n * it was already asked for, and announcing would make the dock take it again at once.\n */\nexport function holdAssistantAsk(ask: AssistantAsk): void {\n pending = ask\n}\n\n/** The dock takes the request once. */\nexport function takeAssistantAsk(): AssistantAsk | undefined {\n const ask = pending\n pending = undefined\n return ask\n}\n\nexport function subscribeAssistantAsk(listener: Listener): () => void {\n listeners.add(listener)\n return () => listeners.delete(listener)\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* What becomes of a request a screen hands the dock - the screen waits on the answer and keeps the person's words
|
|
3
|
+
* unless it was sent (design/ai/shared-dock.md §2-11-1, rule 5).
|
|
4
|
+
*/
|
|
5
|
+
import { askAssistant, peekAssistantAsk, settleAssistantAsk, takeAssistantAsk } from './assistant-ask';
|
|
6
|
+
beforeAll(() => {
|
|
7
|
+
;
|
|
8
|
+
globalThis.window = globalThis.window ?? { dispatchEvent: () => true };
|
|
9
|
+
globalThis.CustomEvent = globalThis.CustomEvent ?? class {
|
|
10
|
+
constructor(type) {
|
|
11
|
+
this.type = type;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => takeAssistantAsk());
|
|
16
|
+
describe('a request and its outcome', () => {
|
|
17
|
+
it('★ the screen hears whether it was sent, and why not', async () => {
|
|
18
|
+
const ask = { ask: 'read them', selection: { title: 'L', records: [{ id: 'a' }] } };
|
|
19
|
+
const outcome = askAssistant(ask);
|
|
20
|
+
expect(peekAssistantAsk()).toBe(ask);
|
|
21
|
+
settleAssistantAsk(takeAssistantAsk(), { sent: false, reason: 'not ready' });
|
|
22
|
+
expect(await outcome).toEqual({ sent: false, reason: 'not ready' });
|
|
23
|
+
});
|
|
24
|
+
it('peeking leaves it waiting; it is settled once', async () => {
|
|
25
|
+
const ask = { ask: 'x' };
|
|
26
|
+
const outcome = askAssistant(ask);
|
|
27
|
+
expect(peekAssistantAsk()).toBe(ask);
|
|
28
|
+
expect(peekAssistantAsk()).toBe(ask);
|
|
29
|
+
settleAssistantAsk(ask, { sent: true });
|
|
30
|
+
settleAssistantAsk(ask, { sent: false, reason: 'late' });
|
|
31
|
+
expect(await outcome).toEqual({ sent: true });
|
|
32
|
+
});
|
|
33
|
+
it('a request replaced before the dock took it is told it was not sent', async () => {
|
|
34
|
+
const first = askAssistant({ ask: 'first' });
|
|
35
|
+
askAssistant({ ask: 'second' });
|
|
36
|
+
expect(await first).toEqual({ sent: false, reason: 'replaced' });
|
|
37
|
+
expect(peekAssistantAsk()?.ask).toBe('second');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=assistant-ask.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-ask.test.js","sourceRoot":"","sources":["../../client/utils/assistant-ask.test.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAEtG,SAAS,CAAC,GAAG,EAAE;IACb,CAAC;IAAC,UAAkB,CAAC,MAAM,GAAI,UAAkB,CAAC,MAAM,IAAI,EAAE,aAAa,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CACxF;IAAC,UAAkB,CAAC,WAAW,GAAI,UAAkB,CAAC,WAAW,IAAI;QAAQ,YAAmB,IAAY;YAAZ,SAAI,GAAJ,IAAI,CAAQ;QAAG,CAAC;KAAE,CAAA;AACrH,CAAC,CAAC,CAAA;AACF,SAAS,CAAC,GAAG,EAAE,CAAC,gBAAgB,EAAE,CAAC,CAAA;AAEnC,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,EAAE,CAAC,qDAAqD,EAAE,KAAK,IAAI,EAAE;QACnE,MAAM,GAAG,GAAG,EAAE,GAAG,EAAE,WAAW,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAA;QACnF,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAA;QACjC,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACpC,kBAAkB,CAAC,gBAAgB,EAAG,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAA;QAC7E,MAAM,CAAC,MAAM,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAA;IACrE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+CAA+C,EAAE,KAAK,IAAI,EAAE;QAC7D,MAAM,GAAG,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAA;QACxB,MAAM,OAAO,GAAG,YAAY,CAAC,GAAG,CAAC,CAAA;QACjC,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACpC,MAAM,CAAC,gBAAgB,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACpC,kBAAkB,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;QACvC,kBAAkB,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;QACxD,MAAM,CAAC,MAAM,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;IAC/C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oEAAoE,EAAE,KAAK,IAAI,EAAE;QAClF,MAAM,KAAK,GAAG,YAAY,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;QAC5C,YAAY,CAAC,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAA;QAC/B,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAA;QAChE,MAAM,CAAC,gBAAgB,EAAE,EAAE,GAAG,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAChD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["/*\n * What becomes of a request a screen hands the dock - the screen waits on the answer and keeps the person's words\n * unless it was sent (design/ai/shared-dock.md §2-11-1, rule 5).\n */\nimport { askAssistant, peekAssistantAsk, settleAssistantAsk, takeAssistantAsk } from './assistant-ask'\n\nbeforeAll(() => {\n ;(globalThis as any).window = (globalThis as any).window ?? { dispatchEvent: () => true }\n ;(globalThis as any).CustomEvent = (globalThis as any).CustomEvent ?? class { constructor(public type: string) {} }\n})\nafterEach(() => takeAssistantAsk())\n\ndescribe('a request and its outcome', () => {\n it('★ the screen hears whether it was sent, and why not', async () => {\n const ask = { ask: 'read them', selection: { title: 'L', records: [{ id: 'a' }] } }\n const outcome = askAssistant(ask)\n expect(peekAssistantAsk()).toBe(ask)\n settleAssistantAsk(takeAssistantAsk()!, { sent: false, reason: 'not ready' })\n expect(await outcome).toEqual({ sent: false, reason: 'not ready' })\n })\n\n it('peeking leaves it waiting; it is settled once', async () => {\n const ask = { ask: 'x' }\n const outcome = askAssistant(ask)\n expect(peekAssistantAsk()).toBe(ask)\n expect(peekAssistantAsk()).toBe(ask)\n settleAssistantAsk(ask, { sent: true })\n settleAssistantAsk(ask, { sent: false, reason: 'late' })\n expect(await outcome).toEqual({ sent: true })\n })\n\n it('a request replaced before the dock took it is told it was not sent', async () => {\n const first = askAssistant({ ask: 'first' })\n askAssistant({ ask: 'second' })\n expect(await first).toEqual({ sent: false, reason: 'replaced' })\n expect(peekAssistantAsk()?.ask).toBe('second')\n })\n})\n"]}
|
|
@@ -25,6 +25,20 @@ export type ProposalHandler = (proposal: any, context: ProposalContext) => Promi
|
|
|
25
25
|
/** Registered by the package that declares the tool - `proposeAction` by twin, a figure candidate by figure-ui. */
|
|
26
26
|
export declare function registerProposalHandler(tool: string, handler: ProposalHandler): void;
|
|
27
27
|
export declare function proposalHandlerOf(tool: string | undefined): ProposalHandler | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* How a package's card is drawn in the conversation, besides the framework's line: an element drawn under it (given
|
|
30
|
+
* `proposal`, `sessionId` and `proposalKey`), and the words of its button. A dashboard card draws its preview as the
|
|
31
|
+
* one looking at the conversation (ADR-0160 decision 6). Registered by the package that declares the tool.
|
|
32
|
+
*/
|
|
33
|
+
export interface ProposalView {
|
|
34
|
+
/** A custom element's tag name - defined by the registering package. */
|
|
35
|
+
previewTag?: string;
|
|
36
|
+
/** i18n key of the button, and its words when the key has none. */
|
|
37
|
+
runLabelKey?: string;
|
|
38
|
+
runLabel?: string;
|
|
39
|
+
}
|
|
40
|
+
export declare function registerProposalView(tool: string, view: ProposalView): void;
|
|
41
|
+
export declare function proposalViewOf(tool: string | undefined): ProposalView | undefined;
|
|
28
42
|
export declare function setEntryProposalHandler(handler: ProposalHandler): void;
|
|
29
43
|
/** Runs the proposal - never throws; a failure is a `refused` outcome with its message as the code. */
|
|
30
44
|
export declare function runProposal(proposal: any, context: ProposalContext): Promise<ProposalOutcome>;
|
|
@@ -6,6 +6,13 @@ export function registerProposalHandler(tool, handler) {
|
|
|
6
6
|
export function proposalHandlerOf(tool) {
|
|
7
7
|
return tool ? handlers.get(tool) : undefined;
|
|
8
8
|
}
|
|
9
|
+
const views = new Map();
|
|
10
|
+
export function registerProposalView(tool, view) {
|
|
11
|
+
views.set(tool, view);
|
|
12
|
+
}
|
|
13
|
+
export function proposalViewOf(tool) {
|
|
14
|
+
return tool ? views.get(tool) : undefined;
|
|
15
|
+
}
|
|
9
16
|
/*
|
|
10
17
|
* Data entry cards are the framework's, whatever record they enter (design/ai/data-entry.md §5): one
|
|
11
18
|
* handler asks the server to enter the stored rows. The dock sets it; no app registers one per record.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-proposals.js","sourceRoot":"","sources":["../../client/utils/assistant-proposals.ts"],"names":[],"mappings":"AA4BA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAA;AAEnD,mHAAmH;AACnH,MAAM,UAAU,uBAAuB,CAAC,IAAY,EAAE,OAAwB;IAC5E,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AAC7B,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAwB;IACxD,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAC9C,CAAC;AAED;;;GAGG;AACH,IAAI,YAAyC,CAAA;AAE7C,MAAM,UAAU,uBAAuB,CAAC,OAAwB;IAC9D,YAAY,GAAG,OAAO,CAAA;AACxB,CAAC;AAED,uGAAuG;AACvG,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAa,EAAE,OAAwB;IACvE,MAAM,OAAO,GAAG,QAAQ,EAAE,IAAI,KAAK,OAAO,IAAI,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC7G,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,uBAAuB,EAAE,CAAA;IACnF,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAChD,OAAO,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,CAAA;IACzF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAG,KAAe,EAAE,OAAO,IAAI,QAAQ,EAAE,CAAA;IACjF,CAAC;AACH,CAAC","sourcesContent":["/*\n * Proposal handlers - each proposal kind is applied by the package that declared its tool\n * (design/ai/shared-dock.md §4). The dock has no kind-specific code: it finds the handler, runs it,\n * and reports the outcome on every branch (ADR-0070). A kind nobody handles is `unavailable`.\n */\nimport type { AssistantSurface } from './assistant-surface.js'\n\nexport type ProposalVerdict = 'accepted' | 'refused' | 'unavailable' | 'declined'\n\nexport interface ProposalOutcome {\n verdict: ProposalVerdict\n errorCode?: string\n applied?: number\n missed?: number\n /** A line for the conversation, besides the outcome itself (e.g. what was created). */\n note?: string\n}\n\nexport interface ProposalContext {\n sessionId?: string\n surface?: AssistantSurface\n /** Asks the person before anything irreversible. `false` → the handler returns `declined`. */\n confirm(options: { title: string; text: string; confirmText?: string; cancelText?: string; type?: 'warning' | 'info' }): Promise<boolean>\n notify(level: 'info' | 'error', message: string): void\n}\n\nexport type ProposalHandler = (proposal: any, context: ProposalContext) => Promise<ProposalOutcome>\n\nconst handlers = new Map<string, ProposalHandler>()\n\n/** Registered by the package that declares the tool - `proposeAction` by twin, a figure candidate by figure-ui. */\nexport function registerProposalHandler(tool: string, handler: ProposalHandler): void {\n handlers.set(tool, handler)\n}\n\nexport function proposalHandlerOf(tool: string | undefined): ProposalHandler | undefined {\n return tool ? handlers.get(tool) : undefined\n}\n\n/*\n * Data entry cards are the framework's, whatever record they enter (design/ai/data-entry.md §5): one\n * handler asks the server to enter the stored rows. The dock sets it; no app registers one per record.\n */\nlet entryHandler: ProposalHandler | undefined\n\nexport function setEntryProposalHandler(handler: ProposalHandler): void {\n entryHandler = handler\n}\n\n/** Runs the proposal - never throws; a failure is a `refused` outcome with its message as the code. */\nexport async function runProposal(proposal: any, context: ProposalContext): Promise<ProposalOutcome> {\n const handler = proposal?.kind === 'entry' && entryHandler ? entryHandler : proposalHandlerOf(proposal?.tool)\n if (!handler) return { verdict: 'unavailable', errorCode: 'unknown-proposal-kind' }\n try {\n const outcome = await handler(proposal, context)\n return outcome?.verdict ? outcome : { verdict: 'unavailable', errorCode: 'no-outcome' }\n } catch (error) {\n return { verdict: 'refused', errorCode: (error as Error)?.message || 'failed' }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"assistant-proposals.js","sourceRoot":"","sources":["../../client/utils/assistant-proposals.ts"],"names":[],"mappings":"AA4BA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAA;AAEnD,mHAAmH;AACnH,MAAM,UAAU,uBAAuB,CAAC,IAAY,EAAE,OAAwB;IAC5E,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AAC7B,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAwB;IACxD,OAAO,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAC9C,CAAC;AAeD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAwB,CAAA;AAE7C,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,IAAkB;IACnE,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;AACvB,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAwB;IACrD,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;AAC3C,CAAC;AAED;;;GAGG;AACH,IAAI,YAAyC,CAAA;AAE7C,MAAM,UAAU,uBAAuB,CAAC,OAAwB;IAC9D,YAAY,GAAG,OAAO,CAAA;AACxB,CAAC;AAED,uGAAuG;AACvG,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAa,EAAE,OAAwB;IACvE,MAAM,OAAO,GAAG,QAAQ,EAAE,IAAI,KAAK,OAAO,IAAI,YAAY,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC7G,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,uBAAuB,EAAE,CAAA;IACnF,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAA;QAChD,OAAO,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,CAAA;IACzF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAG,KAAe,EAAE,OAAO,IAAI,QAAQ,EAAE,CAAA;IACjF,CAAC;AACH,CAAC","sourcesContent":["/*\n * Proposal handlers - each proposal kind is applied by the package that declared its tool\n * (design/ai/shared-dock.md §4). The dock has no kind-specific code: it finds the handler, runs it,\n * and reports the outcome on every branch (ADR-0070). A kind nobody handles is `unavailable`.\n */\nimport type { AssistantSurface } from './assistant-surface.js'\n\nexport type ProposalVerdict = 'accepted' | 'refused' | 'unavailable' | 'declined'\n\nexport interface ProposalOutcome {\n verdict: ProposalVerdict\n errorCode?: string\n applied?: number\n missed?: number\n /** A line for the conversation, besides the outcome itself (e.g. what was created). */\n note?: string\n}\n\nexport interface ProposalContext {\n sessionId?: string\n surface?: AssistantSurface\n /** Asks the person before anything irreversible. `false` → the handler returns `declined`. */\n confirm(options: { title: string; text: string; confirmText?: string; cancelText?: string; type?: 'warning' | 'info' }): Promise<boolean>\n notify(level: 'info' | 'error', message: string): void\n}\n\nexport type ProposalHandler = (proposal: any, context: ProposalContext) => Promise<ProposalOutcome>\n\nconst handlers = new Map<string, ProposalHandler>()\n\n/** Registered by the package that declares the tool - `proposeAction` by twin, a figure candidate by figure-ui. */\nexport function registerProposalHandler(tool: string, handler: ProposalHandler): void {\n handlers.set(tool, handler)\n}\n\nexport function proposalHandlerOf(tool: string | undefined): ProposalHandler | undefined {\n return tool ? handlers.get(tool) : undefined\n}\n\n/**\n * How a package's card is drawn in the conversation, besides the framework's line: an element drawn under it (given\n * `proposal`, `sessionId` and `proposalKey`), and the words of its button. A dashboard card draws its preview as the\n * one looking at the conversation (ADR-0160 decision 6). Registered by the package that declares the tool.\n */\nexport interface ProposalView {\n /** A custom element's tag name - defined by the registering package. */\n previewTag?: string\n /** i18n key of the button, and its words when the key has none. */\n runLabelKey?: string\n runLabel?: string\n}\n\nconst views = new Map<string, ProposalView>()\n\nexport function registerProposalView(tool: string, view: ProposalView): void {\n views.set(tool, view)\n}\n\nexport function proposalViewOf(tool: string | undefined): ProposalView | undefined {\n return tool ? views.get(tool) : undefined\n}\n\n/*\n * Data entry cards are the framework's, whatever record they enter (design/ai/data-entry.md §5): one\n * handler asks the server to enter the stored rows. The dock sets it; no app registers one per record.\n */\nlet entryHandler: ProposalHandler | undefined\n\nexport function setEntryProposalHandler(handler: ProposalHandler): void {\n entryHandler = handler\n}\n\n/** Runs the proposal - never throws; a failure is a `refused` outcome with its message as the code. */\nexport async function runProposal(proposal: any, context: ProposalContext): Promise<ProposalOutcome> {\n const handler = proposal?.kind === 'entry' && entryHandler ? entryHandler : proposalHandlerOf(proposal?.tool)\n if (!handler) return { verdict: 'unavailable', errorCode: 'unknown-proposal-kind' }\n try {\n const outcome = await handler(proposal, context)\n return outcome?.verdict ? outcome : { verdict: 'unavailable', errorCode: 'no-outcome' }\n } catch (error) {\n return { verdict: 'refused', errorCode: (error as Error)?.message || 'failed' }\n }\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The line the model reads begins with this; the conversation does not draw it. */
|
|
2
|
+
export declare const SELECTION_KEYS_PREFIX = "[keys]";
|
|
3
|
+
/** Labels named on the person's line - the rest are counted. */
|
|
4
|
+
export declare const SELECTION_LABELS_SHOWN = 5;
|
|
5
|
+
export interface AssistantSelection {
|
|
6
|
+
/** The list the records were picked from, as the person knows it (「계약 대장」). */
|
|
7
|
+
title: string;
|
|
8
|
+
/** The list's query (`contracts`) - which list the ids belong to, for the model. */
|
|
9
|
+
list?: string;
|
|
10
|
+
records: {
|
|
11
|
+
id: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
export type SelectionTranslate = (key: string, defaultValue: string, options?: Record<string, unknown>) => string;
|
|
16
|
+
/** The note stored in the conversation - the person's line, then the key line. Empty when nothing was picked. */
|
|
17
|
+
export declare function selectionNote(selection: AssistantSelection, t: SelectionTranslate): string;
|
|
18
|
+
/** A note as the conversation draws it - without the line only the model reads. */
|
|
19
|
+
export declare function noteForDisplay(content: string): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The records a person picked on a screen and handed to the assistant - `askAssistant({ selection })`.
|
|
3
|
+
*
|
|
4
|
+
* The conversation keeps them as a note: one line the person reads (「선택 1건 · 계약 대장: H2022019 · 브라이스톤 IT3.0
|
|
5
|
+
* Website」) and one line the model reads - the list and the ids, after `[keys]`. The screen hides the key line; the
|
|
6
|
+
* model is handed the note as stored. Until 2026-10-24 the screen sent the selection as JSON and the conversation
|
|
7
|
+
* opened on `{"source":"explicit-page-selection",…}`.
|
|
8
|
+
*/
|
|
9
|
+
/** The line the model reads begins with this; the conversation does not draw it. */
|
|
10
|
+
export const SELECTION_KEYS_PREFIX = '[keys]';
|
|
11
|
+
/** Labels named on the person's line - the rest are counted. */
|
|
12
|
+
export const SELECTION_LABELS_SHOWN = 5;
|
|
13
|
+
/** The note stored in the conversation - the person's line, then the key line. Empty when nothing was picked. */
|
|
14
|
+
export function selectionNote(selection, t) {
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
const records = (selection?.records ?? []).filter(one => one?.id && !seen.has(one.id) && !!seen.add(one.id));
|
|
17
|
+
if (!records.length)
|
|
18
|
+
return '';
|
|
19
|
+
const labels = records.map(one => (one.label || '').replace(/\s+/g, ' ').trim()).filter(Boolean);
|
|
20
|
+
const shown = labels.slice(0, SELECTION_LABELS_SHOWN).join(', ');
|
|
21
|
+
const rest = labels.length - Math.min(labels.length, SELECTION_LABELS_SHOWN);
|
|
22
|
+
const line = t('selection-note', 'Selected {count} · {title}', { count: records.length, title: selection.title });
|
|
23
|
+
const named = shown ? `${line}: ${shown}${rest > 0 ? ` ${t('selection-more', 'and {count} more', { count: rest })}` : ''}` : line;
|
|
24
|
+
const keys = `${SELECTION_KEYS_PREFIX} ${selection.list ? `${selection.list} ` : ''}${records.map(one => one.id).join(' ')}`;
|
|
25
|
+
return `${named}\n${keys}`;
|
|
26
|
+
}
|
|
27
|
+
/** A note as the conversation draws it - without the line only the model reads. */
|
|
28
|
+
export function noteForDisplay(content) {
|
|
29
|
+
return (content ?? '')
|
|
30
|
+
.split('\n')
|
|
31
|
+
.filter(line => !line.startsWith(SELECTION_KEYS_PREFIX))
|
|
32
|
+
.join('\n')
|
|
33
|
+
.trim();
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=assistant-selection.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { noteForDisplay, selectionNote, SELECTION_KEYS_PREFIX } from './assistant-selection';
|
|
2
|
+
const t = (_key, defaultValue, options = {}) => defaultValue.replace(/\{(\w+)\}/g, (_, name) => String(options[name]));
|
|
3
|
+
const contract = (id, label) => ({ id, label });
|
|
4
|
+
describe('the records picked on a screen, as a note', () => {
|
|
5
|
+
it('★ the person reads a line of names; the model reads the list and the ids', () => {
|
|
6
|
+
const note = selectionNote({ title: '계약 대장', list: 'contracts', records: [contract('c-1', 'H2022019 · 브라이스톤 IT3.0 Website')] }, t);
|
|
7
|
+
expect(note).toBe(`Selected 1 · 계약 대장: H2022019 · 브라이스톤 IT3.0 Website\n${SELECTION_KEYS_PREFIX} contracts c-1`);
|
|
8
|
+
expect(note).not.toMatch(/[{}"]|explicit-page-selection/);
|
|
9
|
+
});
|
|
10
|
+
it('★ the screen draws the person\'s line only', () => {
|
|
11
|
+
const note = selectionNote({ title: '계약 대장', list: 'contracts', records: [contract('c-1', 'H2022019')] }, t);
|
|
12
|
+
expect(noteForDisplay(note)).toBe('Selected 1 · 계약 대장: H2022019');
|
|
13
|
+
expect(noteForDisplay('a plain note\nsecond line')).toBe('a plain note\nsecond line');
|
|
14
|
+
});
|
|
15
|
+
it('every id reaches the model; the names are cut after a few, the rest counted', () => {
|
|
16
|
+
const records = Array.from({ length: 7 }, (_, i) => contract(`c-${i}`, `C${i}`));
|
|
17
|
+
const note = selectionNote({ title: 'L', records }, t);
|
|
18
|
+
expect(note.split('\n')[0]).toBe('Selected 7 · L: C0, C1, C2, C3, C4 and 2 more');
|
|
19
|
+
expect(note.split('\n')[1]).toBe(`${SELECTION_KEYS_PREFIX} ${records.map(one => one.id).join(' ')}`);
|
|
20
|
+
});
|
|
21
|
+
it('a record picked twice is one; nothing picked is no note', () => {
|
|
22
|
+
expect(selectionNote({ title: 'L', records: [contract('a', 'A'), contract('a', 'A')] }, t)).toBe(`Selected 1 · L: A\n${SELECTION_KEYS_PREFIX} a`);
|
|
23
|
+
expect(selectionNote({ title: 'L', records: [] }, t)).toBe('');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
//# sourceMappingURL=assistant-selection.test.js.map
|
|
@@ -0,0 +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"]}
|
|
@@ -13,6 +13,8 @@ export interface AssistantSession {
|
|
|
13
13
|
kind?: string;
|
|
14
14
|
/** Who reads it: 'open' (everyone in the domain), 'group' (the people the owner brought in) or 'private' (the owner alone). The dock draws an open one apart. */
|
|
15
15
|
visibility?: string;
|
|
16
|
+
/** Whether this person sits in it - `false` for someone else's open conversation the dock showed; absent on one just made. */
|
|
17
|
+
participant?: boolean;
|
|
16
18
|
}
|
|
17
19
|
export interface AssistantSessionTarget {
|
|
18
20
|
/** Account/domain boundary supplied by the authenticated host. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-session-controller.js","sourceRoot":"","sources":["../../client/utils/assistant-session-controller.ts"],"names":[],"mappings":"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","sourcesContent":["export interface AssistantSession {\n id: string\n name?: string\n /**\n * 어느 대화인가 — 등록된 모드의 kind (ADR-0072 결정 3).\n *\n * 목록은 한 대상의 대화를 **모두** 보여 준다. 그 대상 위에 대화가 둘 설 수 있으므로(A17), 고른 것이\n * 지금 열려 있는 대화와 다를 수 있다. 그때 화면은 그 대화로 따라가야 한다 — 안 그러면 문이 거절하고\n * (`session-kind-mismatch`), 목록은 눌리는데 안 열리는 목록이 된다.\n *\n * 칸이 서기 전에 난 세션에는 값이 없다. 그때는 지금 대화로 두고, 서버가 다음 턴에 찍는다.\n */\n kind?: string\n /** Who reads it: 'open' (everyone in the domain), 'group' (the people the owner brought in) or 'private' (the owner alone). The dock draws an open one apart. */\n visibility?: string\n}\nexport interface AssistantSessionTarget {\n /** Account/domain boundary supplied by the authenticated host. */\n scope: string\n anchorType: string\n anchorId: string\n /** The conversation kind (mode) the tabs list - all readable conversations of it, not only mine (shared dock §2-6). */\n kind?: string\n}\nexport interface AssistantSessionTransport {\n list(target: AssistantSessionTarget): Promise<AssistantSession[]>\n create(target: AssistantSessionTarget): Promise<AssistantSession>\n isEmpty?(sessionId: string): Promise<boolean>\n /**\n * 이 대화를 여기까지 읽었다 — 열린 대화가 바뀔 때 한 번.\n *\n * 읽은 지점은 **다른 참여자에게 보이는 사실**이다(안 읽음 배지 · 목록의 굵은 글씨). 도크마다 손으로\n * 부르던 것을 여기로 올렸다 — twin 만 부르고 있었고, 멘션으로 사람이 들어올 수 있게 된 뒤에는\n * 세 화면이 같은 사실을 달리 말하게 된다.\n */\n markRead?(sessionId: string): Promise<void> | void\n}\nexport interface AssistantSessionState {\n target?: AssistantSessionTarget\n sessions: AssistantSession[]\n sessionId: string\n loading: boolean\n error: string\n /** Changes on reset, including temporary conversations without a server ID. */\n revision: number\n}\nconst keyOf = (target?: AssistantSessionTarget) => target\n ? JSON.stringify([target.scope, target.anchorType, target.anchorId]) : ''\n\n/** Domain-neutral session lifecycle. Never deletes history or edits host documents. */\nexport class AssistantSessionController {\n state: AssistantSessionState = { sessions: [], sessionId: '', loading: false, error: '', revision: 0 }\n private generation = 0\n private preferred = new Map<string, string>()\n private pending = new Map<string, Promise<AssistantSession[]>>()\n\n private transport: AssistantSessionTransport\n private changed: (state: AssistantSessionState) => void\n private storage?: Pick<Storage, 'getItem' | 'setItem'>\n private lazy: boolean\n private preparing?: Promise<string | undefined>\n constructor(transport: AssistantSessionTransport, changed: (state: AssistantSessionState) => void,\n storage?: Pick<Storage, 'getItem' | 'setItem'>, options: { lazy?: boolean } = {}) {\n this.transport = transport\n this.changed = changed\n this.storage = storage\n this.lazy = !!options.lazy\n }\n\n private remember(key: string, id: string) {\n this.preferred.set(key, id)\n try { this.storage?.setItem('assistant-session:' + key, id) } catch { /* Storage is optional. */ }\n }\n\n private preference(key: string): string | undefined {\n try { return this.preferred.get(key) || this.storage?.getItem('assistant-session:' + key) || undefined }\n catch { return this.preferred.get(key) }\n }\n\n private publish(patch: Partial<AssistantSessionState>) {\n const before = this.state.sessionId\n this.state = { ...this.state, ...patch }\n /*\n * 열린 대화가 바뀌면 읽음 지점을 옮긴다. 한 자리에서 하는 이유는 여는 길이 넷이기 때문이다\n * (열기 · 고르기 · 새로 시작 · 보내며 만들기) — 갈래마다 부르면 한 곳이 빠진다.\n */\n if (this.state.sessionId && this.state.sessionId !== before) void this.markRead(this.state.sessionId)\n this.changed(this.state)\n }\n\n /** 실패는 삼킨다 — 읽음 표시가 못 가는 것이 대화를 막을 이유는 아니다. */\n private async markRead(sessionId: string): Promise<void> {\n try {\n await this.transport.markRead?.(sessionId)\n } catch {\n /* noop */\n }\n }\n\n async open(target?: AssistantSessionTarget, force = false): Promise<void> {\n const key = keyOf(target)\n if (!force && key === keyOf(this.state.target) && (this.state.loading || this.state.sessionId || (this.lazy && this.state.target && !this.state.error))) return\n const generation = ++this.generation\n const snapshot = target ? { ...target } : undefined\n this.publish({ target: snapshot, sessions: [], sessionId: '', loading: !!target, error: '' })\n if (!snapshot) return\n try {\n let pending = this.pending.get(key)\n if (!pending) {\n pending = (async () => {\n const sessions = await this.transport.list(snapshot)\n // Failed/malformed reads must not create replacement sessions.\n if (!Array.isArray(sessions) || sessions.some(session => !session.id)) throw new Error('Invalid session list')\n if (sessions.length || this.lazy) return [...sessions]\n // Do not issue a mutation for a target abandoned while the inbox was loading.\n if (keyOf(this.state.target) !== key || this.pending.get(key) !== pending) throw new Error('Session target changed')\n const created = await this.transport.create(snapshot)\n if (!created?.id) throw new Error('Invalid new session')\n return [created]\n })()\n this.pending.set(key, pending)\n void pending.finally(() => {\n if (this.pending.get(key) === pending) this.pending.delete(key)\n }).catch(() => {})\n }\n const sessions = await pending\n if (generation !== this.generation) return\n const preference = this.preference(key)\n const sessionId = this.lazy && preference === '__draft__' ? '' : sessions.find(session => session.id === preference)?.id || sessions[0]?.id || ''\n this.remember(key, sessionId || '__draft__')\n this.publish({ sessions, sessionId, loading: false })\n } catch (error) {\n if (generation === this.generation) this.publish({ loading: false, error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n select(id: string): boolean {\n if (this.state.loading || !this.state.sessions.some(session => session.id === id)) return false\n this.remember(keyOf(this.state.target), id)\n this.publish({ sessionId: id, error: '' })\n return true\n }\n\n async reset(): Promise<void> {\n if (this.state.loading || this.preparing) return\n if (this.lazy) {\n if (!this.state.sessionId) return\n ++this.generation\n this.remember(keyOf(this.state.target), '__draft__')\n this.publish({ sessionId: '', error: '', revision: this.state.revision + 1 })\n return\n }\n const target = this.state.target\n if (!target) {\n this.publish({ sessions: [], sessionId: '', error: '', revision: this.state.revision + 1 })\n return\n }\n const generation = ++this.generation\n const key = keyOf(target)\n this.publish({ loading: true, error: '' })\n try {\n if (this.state.sessionId && this.transport.isEmpty) {\n const empty = await this.transport.isEmpty(this.state.sessionId)\n if (generation !== this.generation) return\n if (empty) { this.publish({ loading: false }); return }\n }\n const session = await this.transport.create({ ...target })\n if (!session?.id) throw new Error('Invalid new session')\n this.remember(key, session.id)\n if (generation !== this.generation) return\n this.publish({ sessions: [session, ...this.state.sessions.filter(existing => existing.id !== session.id)], sessionId: session.id,\n loading: false, revision: this.state.revision + 1 })\n } catch (error) {\n if (generation === this.generation) this.publish({ loading: false, error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n /** Called only for a real send. Adoption happens before hosts render the new ID. */\n prepareSession(adopt: (id: string) => void = () => {}): Promise<string | undefined> {\n if (this.state.sessionId) return Promise.resolve(this.state.sessionId)\n if (!this.state.target) return Promise.resolve(undefined)\n if (this.state.loading || this.state.error) return Promise.reject(new Error('Session history is not ready'))\n if (this.preparing) return this.preparing\n const target = { ...this.state.target }, generation = this.generation\n const pending = (async () => {\n const session = await this.transport.create(target)\n if (generation !== this.generation) throw new Error('Session target changed')\n if (!session?.id) throw new Error('Invalid new session')\n adopt(session.id)\n this.remember(keyOf(target), session.id)\n this.publish({ sessionId: session.id, sessions: [session, ...this.state.sessions] })\n return session.id\n })()\n this.preparing = pending\n void pending.finally(() => { if (this.preparing === pending) this.preparing = undefined }).catch(() => {})\n return pending\n }\n\n /** Invalidate pending UI updates at logout/disposal. Server authorization remains authoritative. */\n clear() {\n ++this.generation\n this.preferred.clear()\n this.pending.clear()\n this.publish({ target: undefined, sessions: [], sessionId: '', loading: false, error: '', revision: this.state.revision + 1 })\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"assistant-session-controller.js","sourceRoot":"","sources":["../../client/utils/assistant-session-controller.ts"],"names":[],"mappings":"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","sourcesContent":["export interface AssistantSession {\n id: string\n name?: string\n /**\n * 어느 대화인가 — 등록된 모드의 kind (ADR-0072 결정 3).\n *\n * 목록은 한 대상의 대화를 **모두** 보여 준다. 그 대상 위에 대화가 둘 설 수 있으므로(A17), 고른 것이\n * 지금 열려 있는 대화와 다를 수 있다. 그때 화면은 그 대화로 따라가야 한다 — 안 그러면 문이 거절하고\n * (`session-kind-mismatch`), 목록은 눌리는데 안 열리는 목록이 된다.\n *\n * 칸이 서기 전에 난 세션에는 값이 없다. 그때는 지금 대화로 두고, 서버가 다음 턴에 찍는다.\n */\n kind?: string\n /** Who reads it: 'open' (everyone in the domain), 'group' (the people the owner brought in) or 'private' (the owner alone). The dock draws an open one apart. */\n visibility?: string\n /** Whether this person sits in it - `false` for someone else's open conversation the dock showed; absent on one just made. */\n participant?: boolean\n}\nexport interface AssistantSessionTarget {\n /** Account/domain boundary supplied by the authenticated host. */\n scope: string\n anchorType: string\n anchorId: string\n /** The conversation kind (mode) the tabs list - all readable conversations of it, not only mine (shared dock §2-6). */\n kind?: string\n}\nexport interface AssistantSessionTransport {\n list(target: AssistantSessionTarget): Promise<AssistantSession[]>\n create(target: AssistantSessionTarget): Promise<AssistantSession>\n isEmpty?(sessionId: string): Promise<boolean>\n /**\n * 이 대화를 여기까지 읽었다 — 열린 대화가 바뀔 때 한 번.\n *\n * 읽은 지점은 **다른 참여자에게 보이는 사실**이다(안 읽음 배지 · 목록의 굵은 글씨). 도크마다 손으로\n * 부르던 것을 여기로 올렸다 — twin 만 부르고 있었고, 멘션으로 사람이 들어올 수 있게 된 뒤에는\n * 세 화면이 같은 사실을 달리 말하게 된다.\n */\n markRead?(sessionId: string): Promise<void> | void\n}\nexport interface AssistantSessionState {\n target?: AssistantSessionTarget\n sessions: AssistantSession[]\n sessionId: string\n loading: boolean\n error: string\n /** Changes on reset, including temporary conversations without a server ID. */\n revision: number\n}\nconst keyOf = (target?: AssistantSessionTarget) => target\n ? JSON.stringify([target.scope, target.anchorType, target.anchorId]) : ''\n\n/** Domain-neutral session lifecycle. Never deletes history or edits host documents. */\nexport class AssistantSessionController {\n state: AssistantSessionState = { sessions: [], sessionId: '', loading: false, error: '', revision: 0 }\n private generation = 0\n private preferred = new Map<string, string>()\n private pending = new Map<string, Promise<AssistantSession[]>>()\n\n private transport: AssistantSessionTransport\n private changed: (state: AssistantSessionState) => void\n private storage?: Pick<Storage, 'getItem' | 'setItem'>\n private lazy: boolean\n private preparing?: Promise<string | undefined>\n constructor(transport: AssistantSessionTransport, changed: (state: AssistantSessionState) => void,\n storage?: Pick<Storage, 'getItem' | 'setItem'>, options: { lazy?: boolean } = {}) {\n this.transport = transport\n this.changed = changed\n this.storage = storage\n this.lazy = !!options.lazy\n }\n\n private remember(key: string, id: string) {\n this.preferred.set(key, id)\n try { this.storage?.setItem('assistant-session:' + key, id) } catch { /* Storage is optional. */ }\n }\n\n private preference(key: string): string | undefined {\n try { return this.preferred.get(key) || this.storage?.getItem('assistant-session:' + key) || undefined }\n catch { return this.preferred.get(key) }\n }\n\n private publish(patch: Partial<AssistantSessionState>) {\n const before = this.state.sessionId\n this.state = { ...this.state, ...patch }\n /*\n * 열린 대화가 바뀌면 읽음 지점을 옮긴다. 한 자리에서 하는 이유는 여는 길이 넷이기 때문이다\n * (열기 · 고르기 · 새로 시작 · 보내며 만들기) — 갈래마다 부르면 한 곳이 빠진다.\n */\n if (this.state.sessionId && this.state.sessionId !== before) void this.markRead(this.state.sessionId)\n this.changed(this.state)\n }\n\n /** 실패는 삼킨다 — 읽음 표시가 못 가는 것이 대화를 막을 이유는 아니다. */\n private async markRead(sessionId: string): Promise<void> {\n try {\n await this.transport.markRead?.(sessionId)\n } catch {\n /* noop */\n }\n }\n\n async open(target?: AssistantSessionTarget, force = false): Promise<void> {\n const key = keyOf(target)\n if (!force && key === keyOf(this.state.target) && (this.state.loading || this.state.sessionId || (this.lazy && this.state.target && !this.state.error))) return\n const generation = ++this.generation\n const snapshot = target ? { ...target } : undefined\n this.publish({ target: snapshot, sessions: [], sessionId: '', loading: !!target, error: '' })\n if (!snapshot) return\n try {\n let pending = this.pending.get(key)\n if (!pending) {\n pending = (async () => {\n const sessions = await this.transport.list(snapshot)\n // Failed/malformed reads must not create replacement sessions.\n if (!Array.isArray(sessions) || sessions.some(session => !session.id)) throw new Error('Invalid session list')\n if (sessions.length || this.lazy) return [...sessions]\n // Do not issue a mutation for a target abandoned while the inbox was loading.\n if (keyOf(this.state.target) !== key || this.pending.get(key) !== pending) throw new Error('Session target changed')\n const created = await this.transport.create(snapshot)\n if (!created?.id) throw new Error('Invalid new session')\n return [created]\n })()\n this.pending.set(key, pending)\n void pending.finally(() => {\n if (this.pending.get(key) === pending) this.pending.delete(key)\n }).catch(() => {})\n }\n const sessions = await pending\n if (generation !== this.generation) return\n const preference = this.preference(key)\n const sessionId = this.lazy && preference === '__draft__' ? '' : sessions.find(session => session.id === preference)?.id || sessions[0]?.id || ''\n this.remember(key, sessionId || '__draft__')\n this.publish({ sessions, sessionId, loading: false })\n } catch (error) {\n if (generation === this.generation) this.publish({ loading: false, error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n select(id: string): boolean {\n if (this.state.loading || !this.state.sessions.some(session => session.id === id)) return false\n this.remember(keyOf(this.state.target), id)\n this.publish({ sessionId: id, error: '' })\n return true\n }\n\n async reset(): Promise<void> {\n if (this.state.loading || this.preparing) return\n if (this.lazy) {\n if (!this.state.sessionId) return\n ++this.generation\n this.remember(keyOf(this.state.target), '__draft__')\n this.publish({ sessionId: '', error: '', revision: this.state.revision + 1 })\n return\n }\n const target = this.state.target\n if (!target) {\n this.publish({ sessions: [], sessionId: '', error: '', revision: this.state.revision + 1 })\n return\n }\n const generation = ++this.generation\n const key = keyOf(target)\n this.publish({ loading: true, error: '' })\n try {\n if (this.state.sessionId && this.transport.isEmpty) {\n const empty = await this.transport.isEmpty(this.state.sessionId)\n if (generation !== this.generation) return\n if (empty) { this.publish({ loading: false }); return }\n }\n const session = await this.transport.create({ ...target })\n if (!session?.id) throw new Error('Invalid new session')\n this.remember(key, session.id)\n if (generation !== this.generation) return\n this.publish({ sessions: [session, ...this.state.sessions.filter(existing => existing.id !== session.id)], sessionId: session.id,\n loading: false, revision: this.state.revision + 1 })\n } catch (error) {\n if (generation === this.generation) this.publish({ loading: false, error: error instanceof Error ? error.message : String(error) })\n }\n }\n\n /** Called only for a real send. Adoption happens before hosts render the new ID. */\n prepareSession(adopt: (id: string) => void = () => {}): Promise<string | undefined> {\n if (this.state.sessionId) return Promise.resolve(this.state.sessionId)\n if (!this.state.target) return Promise.resolve(undefined)\n if (this.state.loading || this.state.error) return Promise.reject(new Error('Session history is not ready'))\n if (this.preparing) return this.preparing\n const target = { ...this.state.target }, generation = this.generation\n const pending = (async () => {\n const session = await this.transport.create(target)\n if (generation !== this.generation) throw new Error('Session target changed')\n if (!session?.id) throw new Error('Invalid new session')\n adopt(session.id)\n this.remember(keyOf(target), session.id)\n this.publish({ sessionId: session.id, sessions: [session, ...this.state.sessions] })\n return session.id\n })()\n this.preparing = pending\n void pending.finally(() => { if (this.preparing === pending) this.preparing = undefined }).catch(() => {})\n return pending\n }\n\n /** Invalidate pending UI updates at logout/disposal. Server authorization remains authoritative. */\n clear() {\n ++this.generation\n this.preferred.clear()\n this.pending.clear()\n this.publish({ target: undefined, sessions: [], sessionId: '', loading: false, error: '', revision: this.state.revision + 1 })\n }\n}\n"]}
|
|
@@ -20,7 +20,7 @@ export const assistantSessionTransport = {
|
|
|
20
20
|
* Every readable conversation of this kind on the anchor, not only mine - the same scope the manager
|
|
21
21
|
* shows by default (design/ai/shared-dock.md §2-6). Without a kind: every kind, as before.
|
|
22
22
|
*/
|
|
23
|
-
query: gql `query($type:String!,$id:String!,$kind:String){chatSessionInbox(anchorType:$type,anchorId:$id,kind:$kind,limit:100){id name kind visibility}}`,
|
|
23
|
+
query: gql `query($type:String!,$id:String!,$kind:String){chatSessionInbox(anchorType:$type,anchorId:$id,kind:$kind,limit:100){id name kind visibility participant}}`,
|
|
24
24
|
variables: { type: target.anchorType, id: target.anchorId, kind: target.kind || null }, fetchPolicy: 'no-cache'
|
|
25
25
|
});
|
|
26
26
|
if (result.errors?.length)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"assistant-session-transport.js","sourceRoot":"","sources":["../../client/utils/assistant-session-transport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,GAAG,MAAM,aAAa,CAAA;AAG7B,6FAA6F;AAC7F,MAAM,CAAC,MAAM,yBAAyB,GAA8B;IAClE,KAAK,CAAC,OAAO,CAAC,SAAS;QACrB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAChC,KAAK,EAAE,GAAG,CAAA,6DAA6D;YACvE,SAAS,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,WAAW,EAAE,UAAU;SACtD,CAAC,CAAA;QACF,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;QACpE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,uCAAuC,EAAE,EAAE,YAAY,EAAE,mCAAmC,EAAE,CAAC,CAAC,CAAA;QACzK,OAAO,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC,CAAA;IAC9C,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,MAAM;QACf,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC;YAChC;;;eAGG;YACH,KAAK,EAAE,GAAG,CAAA,
|
|
1
|
+
{"version":3,"file":"assistant-session-transport.js","sourceRoot":"","sources":["../../client/utils/assistant-session-transport.ts"],"names":[],"mappings":"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","sourcesContent":["import { client } from '@operato/graphql'\nimport { i18next } from '@operato/i18n'\nimport gql from 'graphql-tag'\nimport type { AssistantSessionTransport } from './assistant-session-controller'\n\n/** Existing authorized session API; does not introduce a product-specific storage format. */\nexport const assistantSessionTransport: AssistantSessionTransport = {\n async isEmpty(sessionId) {\n const result = await client.query({\n query: gql`query($id:String!){chatMessages(sessionId:$id,limit:1){id}}`,\n variables: { id: sessionId }, fetchPolicy: 'no-cache'\n })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n if (!Array.isArray(result.data?.chatMessages)) throw new Error(i18next.t('ai-assistant.text.messages-unreadable', { defaultValue: 'could not read this conversation.' }))\n return result.data.chatMessages.length === 0\n },\n async list(target) {\n const result = await client.query({\n /*\n * Every readable conversation of this kind on the anchor, not only mine - the same scope the manager\n * shows by default (design/ai/shared-dock.md §2-6). Without a kind: every kind, as before.\n */\n query: gql`query($type:String!,$id:String!,$kind:String){chatSessionInbox(anchorType:$type,anchorId:$id,kind:$kind,limit:100){id name kind visibility participant}}`,\n variables: { type: target.anchorType, id: target.anchorId, kind: target.kind || null }, fetchPolicy: 'no-cache'\n })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n if (!Array.isArray(result.data?.chatSessionInbox)) throw new Error(i18next.t('ai-assistant.text.sessions-unreadable', { defaultValue: 'could not load the conversation list.' }))\n return result.data.chatSessionInbox\n },\n async markRead(sessionId) {\n /* 참여자가 아닌 대화(내 읽음 지점이 없는 자리)면 서버가 false 를 돌려준다 — 그것도 답이다. */\n await client.mutate({\n mutation: gql`mutation($id:String!){markSessionRead(sessionId:$id)}`,\n variables: { id: sessionId }\n })\n },\n async create(target) {\n const result = await client.mutate({\n mutation: gql`mutation($type:String!,$id:String!){createAISession(anchorType:$type,anchorId:$id){id name kind visibility}}`,\n variables: { type: target.anchorType, id: target.anchorId }\n })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n if (!result.data?.createAISession?.id) throw new Error(i18next.t('ai-assistant.text.session-not-created', { defaultValue: 'could not start a conversation.' }))\n return result.data.createAISession\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Pressing an invite card seats the person through the invite door, as the one who pressed (ADR-0073 decision 3).
|
|
3
|
+
* The card is ai-assistant's own tool (server: people/invite-tools.ts), so its handler is registered here, by the
|
|
4
|
+
* package that declared it - the dock knows no proposal kind (design/ai/shared-dock.md §4).
|
|
5
|
+
*/
|
|
6
|
+
import gql from 'graphql-tag';
|
|
7
|
+
import { i18next } from '@operato/i18n';
|
|
8
|
+
import { client } from '@operato/graphql';
|
|
9
|
+
import { registerProposalHandler } from './assistant-proposals.js';
|
|
10
|
+
import { inviteProposalHandler } from './invite-proposal.js';
|
|
11
|
+
registerProposalHandler('proposeInvite', inviteProposalHandler(async (sessionId, userIds) => {
|
|
12
|
+
const result = await client.mutate({
|
|
13
|
+
mutation: gql `
|
|
14
|
+
mutation ($id: String!, $users: [String!]!) {
|
|
15
|
+
inviteToAISession(sessionId: $id, userIds: $users)
|
|
16
|
+
}
|
|
17
|
+
`,
|
|
18
|
+
variables: { id: sessionId, users: userIds }
|
|
19
|
+
});
|
|
20
|
+
if (result.errors?.length)
|
|
21
|
+
throw new Error(result.errors[0].message);
|
|
22
|
+
return result.data?.inviteToAISession ?? [];
|
|
23
|
+
}, (key, options) => String(i18next.t(key, options))));
|
|
24
|
+
//# sourceMappingURL=invite-proposal-register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invite-proposal-register.js","sourceRoot":"","sources":["../../client/utils/invite-proposal-register.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,GAAG,MAAM,aAAa,CAAA;AAE7B,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AACvC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAEzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAA;AAClE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAE5D,uBAAuB,CACrB,eAAe,EACf,qBAAqB,CACnB,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,EAAE;IAC3B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC;QACjC,QAAQ,EAAE,GAAG,CAAA;;;;SAIZ;QACD,SAAS,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE;KAC7C,CAAC,CAAA;IACF,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM;QAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAA;IACpE,OAAO,MAAM,CAAC,IAAI,EAAE,iBAAiB,IAAI,EAAE,CAAA;AAC7C,CAAC,EACD,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAClD,CACF,CAAA","sourcesContent":["/*\n * Pressing an invite card seats the person through the invite door, as the one who pressed (ADR-0073 decision 3).\n * The card is ai-assistant's own tool (server: people/invite-tools.ts), so its handler is registered here, by the\n * package that declared it - the dock knows no proposal kind (design/ai/shared-dock.md §4).\n */\nimport gql from 'graphql-tag'\n\nimport { i18next } from '@operato/i18n'\nimport { client } from '@operato/graphql'\n\nimport { registerProposalHandler } from './assistant-proposals.js'\nimport { inviteProposalHandler } from './invite-proposal.js'\n\nregisterProposalHandler(\n 'proposeInvite',\n inviteProposalHandler(\n async (sessionId, userIds) => {\n const result = await client.mutate({\n mutation: gql`\n mutation ($id: String!, $users: [String!]!) {\n inviteToAISession(sessionId: $id, userIds: $users)\n }\n `,\n variables: { id: sessionId, users: userIds }\n })\n if (result.errors?.length) throw new Error(result.errors[0].message)\n return result.data?.inviteToAISession ?? []\n },\n (key, options) => String(i18next.t(key, options))\n )\n)\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ProposalHandler } from './assistant-proposals.js';
|
|
2
|
+
export interface InvitePerson {
|
|
3
|
+
userId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
department?: string;
|
|
6
|
+
position?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function isInviteProposal(proposal: any): boolean;
|
|
9
|
+
/** The people a press brings in: the one picked, or the only one on the card. */
|
|
10
|
+
export declare function invitedBy(proposal: any): InvitePerson[];
|
|
11
|
+
/** One line under a person's name - where they work. */
|
|
12
|
+
export declare function personLine(person: InvitePerson): string;
|
|
13
|
+
/**
|
|
14
|
+
* Presses the card through the invite door. `invite` is the door - `inviteToAISession(sessionId, userIds)`, answering the
|
|
15
|
+
* ids seated now; it throws the door's refusal, which the dock reports as `refused` with that message.
|
|
16
|
+
*/
|
|
17
|
+
export declare function inviteProposalHandler(invite: (sessionId: string, userIds: string[]) => Promise<string[]>, t: (key: string, options: Record<string, unknown>) => string): ProposalHandler;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function isInviteProposal(proposal) {
|
|
2
|
+
return proposal?.kind === 'invite' && proposal?.tool === 'proposeInvite' && Array.isArray(proposal?.people) && proposal.people.length > 0;
|
|
3
|
+
}
|
|
4
|
+
/** The people a press brings in: the one picked, or the only one on the card. */
|
|
5
|
+
export function invitedBy(proposal) {
|
|
6
|
+
const people = Array.isArray(proposal?.people) ? proposal.people.filter((one) => one?.userId) : [];
|
|
7
|
+
if (proposal?.chosen)
|
|
8
|
+
return people.filter(one => one.userId === proposal.chosen);
|
|
9
|
+
return people.length === 1 ? people : [];
|
|
10
|
+
}
|
|
11
|
+
/** One line under a person's name - where they work. */
|
|
12
|
+
export function personLine(person) {
|
|
13
|
+
return [person.department, person.position].filter(Boolean).join(' · ');
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Presses the card through the invite door. `invite` is the door - `inviteToAISession(sessionId, userIds)`, answering the
|
|
17
|
+
* ids seated now; it throws the door's refusal, which the dock reports as `refused` with that message.
|
|
18
|
+
*/
|
|
19
|
+
export function inviteProposalHandler(invite, t) {
|
|
20
|
+
return async (proposal, context) => {
|
|
21
|
+
const people = invitedBy(proposal);
|
|
22
|
+
if (!people.length)
|
|
23
|
+
return { verdict: 'refused', errorCode: 'no-person-chosen' };
|
|
24
|
+
if (!context.sessionId)
|
|
25
|
+
return { verdict: 'unavailable', errorCode: 'no-conversation' };
|
|
26
|
+
const seated = await invite(context.sessionId, people.map(one => one.userId));
|
|
27
|
+
if (!seated.length)
|
|
28
|
+
return { verdict: 'refused', errorCode: 'already-here' };
|
|
29
|
+
const names = people.filter(one => seated.includes(one.userId)).map(one => one.name).join(', ');
|
|
30
|
+
return {
|
|
31
|
+
verdict: 'accepted',
|
|
32
|
+
applied: seated.length,
|
|
33
|
+
note: t('ai-assistant.people.invited', { names, defaultValue: '{names} joined this conversation and was told.' })
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=invite-proposal.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invite-proposal.js","sourceRoot":"","sources":["../../client/utils/invite-proposal.ts"],"names":[],"mappings":"AAgBA,MAAM,UAAU,gBAAgB,CAAC,QAAa;IAC5C,OAAO,QAAQ,EAAE,IAAI,KAAK,QAAQ,IAAI,QAAQ,EAAE,IAAI,KAAK,eAAe,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;AAC3I,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,SAAS,CAAC,QAAa;IACrC,MAAM,MAAM,GAAmB,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACvH,IAAI,QAAQ,EAAE,MAAM;QAAE,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM,CAAC,CAAA;IACjF,OAAO,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAA;AAC1C,CAAC;AAED,wDAAwD;AACxD,MAAM,UAAU,UAAU,CAAC,MAAoB;IAC7C,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;AACzE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAAmE,EACnE,CAA4D;IAE5D,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE;QACjC,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAA;QAClC,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAA;QAChF,IAAI,CAAC,OAAO,CAAC,SAAS;YAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAA;QACvF,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;QAC7E,IAAI,CAAC,MAAM,CAAC,MAAM;YAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,CAAA;QAC5E,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC/F,OAAO;YACL,OAAO,EAAE,UAAU;YACnB,OAAO,EAAE,MAAM,CAAC,MAAM;YACtB,IAAI,EAAE,CAAC,CAAC,6BAA6B,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,gDAAgD,EAAE,CAAC;SAClH,CAAA;IACH,CAAC,CAAA;AACH,CAAC","sourcesContent":["/*\n * The invite card (server: people/invite-tools.ts) - who it offers, and what pressing it does.\n *\n * Pressing is the person's act: the dock calls `inviteToAISession` with their own request, and the door decides\n * whether they may seat anyone here (`maySeat`). The model's card seats nobody by itself. A card with several people\n * of one name is pressed on one of them - the press carries `chosen`; nothing is guessed.\n */\nimport type { ProposalHandler } from './assistant-proposals.js'\n\nexport interface InvitePerson {\n userId: string\n name: string\n department?: string\n position?: string\n}\n\nexport function isInviteProposal(proposal: any): boolean {\n return proposal?.kind === 'invite' && proposal?.tool === 'proposeInvite' && Array.isArray(proposal?.people) && proposal.people.length > 0\n}\n\n/** The people a press brings in: the one picked, or the only one on the card. */\nexport function invitedBy(proposal: any): InvitePerson[] {\n const people: InvitePerson[] = Array.isArray(proposal?.people) ? proposal.people.filter((one: any) => one?.userId) : []\n if (proposal?.chosen) return people.filter(one => one.userId === proposal.chosen)\n return people.length === 1 ? people : []\n}\n\n/** One line under a person's name - where they work. */\nexport function personLine(person: InvitePerson): string {\n return [person.department, person.position].filter(Boolean).join(' · ')\n}\n\n/**\n * Presses the card through the invite door. `invite` is the door - `inviteToAISession(sessionId, userIds)`, answering the\n * ids seated now; it throws the door's refusal, which the dock reports as `refused` with that message.\n */\nexport function inviteProposalHandler(\n invite: (sessionId: string, userIds: string[]) => Promise<string[]>,\n t: (key: string, options: Record<string, unknown>) => string\n): ProposalHandler {\n return async (proposal, context) => {\n const people = invitedBy(proposal)\n if (!people.length) return { verdict: 'refused', errorCode: 'no-person-chosen' }\n if (!context.sessionId) return { verdict: 'unavailable', errorCode: 'no-conversation' }\n const seated = await invite(context.sessionId, people.map(one => one.userId))\n if (!seated.length) return { verdict: 'refused', errorCode: 'already-here' }\n const names = people.filter(one => seated.includes(one.userId)).map(one => one.name).join(', ')\n return {\n verdict: 'accepted',\n applied: seated.length,\n note: t('ai-assistant.people.invited', { names, defaultValue: '{names} joined this conversation and was told.' })\n }\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { inviteProposalHandler, invitedBy, isInviteProposal } from './invite-proposal';
|
|
2
|
+
const card = (people, extra = {}) => ({ tool: 'proposeInvite', kind: 'invite', people, ...extra });
|
|
3
|
+
const kim = { userId: 'u-1', name: '정연희', department: 'Sales' };
|
|
4
|
+
const lee = { userId: 'u-2', name: '정연희', department: 'Finance' };
|
|
5
|
+
const t = (_key, options) => String(options.defaultValue).replace('{names}', options.names);
|
|
6
|
+
const context = (sessionId) => ({ sessionId, confirm: async () => true, notify: () => { } });
|
|
7
|
+
describe('the invite card', () => {
|
|
8
|
+
it('is the invite tool\'s card with someone on it', () => {
|
|
9
|
+
expect(isInviteProposal(card([kim]))).toBe(true);
|
|
10
|
+
expect(isInviteProposal(card([]))).toBe(false);
|
|
11
|
+
expect(isInviteProposal({ ...card([kim]), tool: 'proposeAction' })).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
it('★ one person: pressing invites them; several: only the one picked - nothing is guessed', () => {
|
|
14
|
+
expect(invitedBy(card([kim]))).toEqual([kim]);
|
|
15
|
+
expect(invitedBy(card([kim, lee]))).toEqual([]);
|
|
16
|
+
expect(invitedBy(card([kim, lee], { chosen: 'u-2' }))).toEqual([lee]);
|
|
17
|
+
});
|
|
18
|
+
it('★ pressing calls the invite door with the conversation and the person picked', async () => {
|
|
19
|
+
const calls = [];
|
|
20
|
+
const handler = inviteProposalHandler(async (sessionId, userIds) => (calls.push([sessionId, userIds]), userIds), t);
|
|
21
|
+
expect(await handler(card([kim, lee], { chosen: 'u-2' }), context('s1'))).toMatchObject({ verdict: 'accepted', applied: 1, note: '정연희 joined this conversation and was told.' });
|
|
22
|
+
expect(calls).toEqual([['s1', ['u-2']]]);
|
|
23
|
+
});
|
|
24
|
+
it('no pick, no conversation, or nobody seated - said, never accepted', async () => {
|
|
25
|
+
const handler = inviteProposalHandler(async (_s, userIds) => userIds, t);
|
|
26
|
+
expect(await handler(card([kim, lee]), context('s1'))).toEqual({ verdict: 'refused', errorCode: 'no-person-chosen' });
|
|
27
|
+
expect(await handler(card([kim]), context())).toEqual({ verdict: 'unavailable', errorCode: 'no-conversation' });
|
|
28
|
+
expect(await inviteProposalHandler(async () => [], t)(card([kim]), context('s1'))).toEqual({ verdict: 'refused', errorCode: 'already-here' });
|
|
29
|
+
});
|
|
30
|
+
it("the door's refusal is not swallowed - it is thrown to the dock, which reports it", async () => {
|
|
31
|
+
const handler = inviteProposalHandler(async () => { throw new Error('only the person who started this group conversation can bring people in.'); }, t);
|
|
32
|
+
await expect(handler(card([kim]), context('s1'))).rejects.toThrow(/only the person who started/);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
//# sourceMappingURL=invite-proposal.test.js.map
|