@things-factory/ai-assistant 10.1.28 β†’ 10.1.31

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 (110) hide show
  1. package/client/components/assistant-chat.ts +51 -7
  2. package/client/components/assistant-mode.test.ts +20 -1
  3. package/client/components/assistant-mode.ts +20 -0
  4. package/client/components/panel-aria.test.ts +20 -0
  5. package/client/utils/assistant-session-controller.ts +14 -1
  6. package/client/utils/assistant-session-transport.ts +2 -2
  7. package/dist-client/components/assistant-chat.d.ts +12 -0
  8. package/dist-client/components/assistant-chat.js +48 -6
  9. package/dist-client/components/assistant-chat.js.map +1 -1
  10. package/dist-client/components/assistant-mode.d.ts +22 -0
  11. package/dist-client/components/assistant-mode.js +16 -0
  12. package/dist-client/components/assistant-mode.js.map +1 -1
  13. package/dist-client/components/assistant-mode.test.js +17 -1
  14. package/dist-client/components/assistant-mode.test.js.map +1 -1
  15. package/dist-client/components/panel-aria.test.js +18 -0
  16. package/dist-client/components/panel-aria.test.js.map +1 -1
  17. package/dist-client/tsconfig.tsbuildinfo +1 -1
  18. package/dist-client/utils/assistant-session-controller.d.ts +10 -0
  19. package/dist-client/utils/assistant-session-controller.js.map +1 -1
  20. package/dist-client/utils/assistant-session-transport.js +2 -2
  21. package/dist-client/utils/assistant-session-transport.js.map +1 -1
  22. package/dist-server/door-declaration-report.d.ts +14 -0
  23. package/dist-server/door-declaration-report.js +54 -0
  24. package/dist-server/door-declaration-report.js.map +1 -0
  25. package/dist-server/service/assistant-chat-resolver.js +19 -0
  26. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  27. package/dist-server/service/assistant-mode/assistant-mode-resolver.d.ts +8 -0
  28. package/dist-server/service/assistant-mode/assistant-mode-resolver.js +8 -0
  29. package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -1
  30. package/dist-server/service/assistant-mode/index.d.ts +1 -1
  31. package/dist-server/service/assistant-mode/index.js +1 -1
  32. package/dist-server/service/assistant-mode/index.js.map +1 -1
  33. package/dist-server/service/assistant-mode/resolve.d.ts +10 -0
  34. package/dist-server/service/assistant-mode/resolve.js +5 -0
  35. package/dist-server/service/assistant-mode/resolve.js.map +1 -1
  36. package/dist-server/service/assistant-mode/types.d.ts +21 -0
  37. package/dist-server/service/assistant-mode/types.js.map +1 -1
  38. package/dist-server/service/chat-message/carried-turns.js +14 -2
  39. package/dist-server/service/chat-message/carried-turns.js.map +1 -1
  40. package/dist-server/service/chat-message/door-access.d.ts +19 -7
  41. package/dist-server/service/chat-message/door-access.js +19 -11
  42. package/dist-server/service/chat-message/door-access.js.map +1 -1
  43. package/dist-server/service/chat-message/reader-gate.js +6 -4
  44. package/dist-server/service/chat-message/reader-gate.js.map +1 -1
  45. package/dist-server/service/chat-session/chat-session.d.ts +12 -0
  46. package/dist-server/service/chat-session/chat-session.js +12 -11
  47. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  48. package/dist-server/service/chat-session/session-anchor.d.ts +10 -1
  49. package/dist-server/service/chat-session/session-anchor.js +12 -4
  50. package/dist-server/service/chat-session/session-anchor.js.map +1 -1
  51. package/dist-server/service/chat-session/session-inbox.d.ts +2 -0
  52. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  53. package/dist-server/service/chat-session/session-kind.d.ts +14 -0
  54. package/dist-server/service/chat-session/session-kind.js +30 -0
  55. package/dist-server/service/chat-session/session-kind.js.map +1 -0
  56. package/dist-server/service/chat-session/session-visibility.js +8 -1
  57. package/dist-server/service/chat-session/session-visibility.js.map +1 -1
  58. package/dist-server/service/chat-session-resolver.d.ts +2 -0
  59. package/dist-server/service/chat-session-resolver.js +4 -0
  60. package/dist-server/service/chat-session-resolver.js.map +1 -1
  61. package/dist-server/service/index.d.ts +1 -0
  62. package/dist-server/service/index.js +2 -0
  63. package/dist-server/service/index.js.map +1 -1
  64. package/dist-server/service/testing/conversation-script.d.ts +7 -0
  65. package/dist-server/service/testing/conversation-script.js +1 -0
  66. package/dist-server/service/testing/conversation-script.js.map +1 -1
  67. package/dist-server/service/testing/mode-conformance.d.ts +10 -0
  68. package/dist-server/service/testing/mode-conformance.js +13 -0
  69. package/dist-server/service/testing/mode-conformance.js.map +1 -1
  70. package/dist-server/service/tool-access.js +20 -7
  71. package/dist-server/service/tool-access.js.map +1 -1
  72. package/dist-server/tool-registry-boot-report.js +15 -9
  73. package/dist-server/tool-registry-boot-report.js.map +1 -1
  74. package/dist-server/tsconfig.tsbuildinfo +1 -1
  75. package/package.json +5 -5
  76. package/server/door-declaration-report.test.ts +53 -0
  77. package/server/door-declaration-report.ts +61 -0
  78. package/server/service/assistant-chat-resolver.ts +21 -0
  79. package/server/service/assistant-mode/assistant-mode-resolver.ts +6 -0
  80. package/server/service/assistant-mode/index.ts +1 -1
  81. package/server/service/assistant-mode/resolve.test.ts +42 -0
  82. package/server/service/assistant-mode/resolve.ts +9 -0
  83. package/server/service/assistant-mode/types.ts +18 -0
  84. package/server/service/chat-message/carried-turns.test.ts +13 -2
  85. package/server/service/chat-message/carried-turns.ts +16 -3
  86. package/server/service/chat-message/door-access.test.ts +72 -0
  87. package/server/service/chat-message/door-access.ts +43 -23
  88. package/server/service/chat-message/fold-history-window.test.ts +184 -0
  89. package/server/service/chat-message/reader-gate.ts +6 -4
  90. package/server/service/chat-session/chat-session.ts +15 -0
  91. package/server/service/chat-session/session-anchor.ts +12 -3
  92. package/server/service/chat-session/session-inbox.ts +2 -0
  93. package/server/service/chat-session/session-kind.test.ts +24 -0
  94. package/server/service/chat-session/session-kind.ts +32 -0
  95. package/server/service/chat-session/session-visibility.test.ts +16 -0
  96. package/server/service/chat-session/session-visibility.ts +9 -1
  97. package/server/service/chat-session-resolver.ts +4 -0
  98. package/server/service/index.ts +2 -0
  99. package/server/service/testing/conversation-scenario.test.ts +2 -1
  100. package/server/service/testing/conversation-script.test.ts +4 -2
  101. package/server/service/testing/conversation-script.ts +8 -0
  102. package/server/service/testing/mode-conformance.ts +15 -0
  103. package/server/service/tool-access.test.ts +67 -19
  104. package/server/service/tool-access.ts +24 -15
  105. package/server/tool-registry-boot-report.ts +14 -9
  106. package/translations/en.json +3 -2
  107. package/translations/ja.json +3 -2
  108. package/translations/ko.json +3 -2
  109. package/translations/ms.json +3 -2
  110. package/translations/zh.json +3 -2
@@ -44,6 +44,16 @@ export interface AssistantModeView {
44
44
  catalogEntries: any[];
45
45
  proposalChoices: any[];
46
46
  placeholder?: string;
47
+ /** 빈 λŒ€ν™”μ˜ 제λͺ©κ³Ό ν•œ 쀄 β€” 이 λŒ€ν™”κ°€ 무엇을 ν•˜λŠ” μžλ¦¬μΈμ§€. host 속성보닀 μ•žμ„ λ‹€. */
48
+ intro?: {
49
+ title: string;
50
+ subtitle: string;
51
+ };
52
+ /** 빈 λŒ€ν™”μ˜ μ˜ˆμ‹œ β€” 이 λŒ€ν™”μ—μ„œ μ‹€μ œλ‘œ ν•  수 μžˆλŠ” 말. host 속성보닀 μ•žμ„ λ‹€. */
53
+ examples?: Array<{
54
+ label: string;
55
+ items: string[];
56
+ }>;
47
57
  footerNotice?: string;
48
58
  stagedNotice?: string;
49
59
  }
@@ -107,3 +117,15 @@ export interface AssistantModeChoice {
107
117
  * κ³ λ₯Ό 것이 ν•˜λ‚˜λΏμ΄λ©΄ 기얡을 보지 μ•ŠλŠ”λ‹€ β€” κ³ λ₯Έ 적이 μ—†λŠ” 화면이닀.
108
118
  */
109
119
  export declare function pickInitialMode(current: string | undefined, choices: ReadonlyArray<AssistantModeChoice>, remembered: string | undefined): string | undefined;
120
+ /**
121
+ * λͺ©λ‘μ—μ„œ κ³ λ₯Έ λŒ€ν™”λ‘œ λ”°λΌκ°ˆ 것인가 β€” ADR-0072 κ²°μ • 3.
122
+ *
123
+ * λͺ©λ‘μ€ ν•œ λŒ€μƒμ˜ λŒ€ν™”λ₯Ό **λͺ¨λ‘** 보여 μ£Όκ³ , κ·Έ λŒ€μƒ μœ„μ—λŠ” λŒ€ν™”κ°€ λ‘˜ μ„€ 수 μžˆλ‹€(A17). κ³ λ₯Έ 것이 μ§€κΈˆ
124
+ * μ—΄λ¦° λŒ€ν™”μ™€ λ‹€λ₯Έλ° 화면이 κ·ΈλŒ€λ‘œ 있으면 문이 κ±°μ ˆν•˜κ³ (`session-kind-mismatch`), λͺ©λ‘μ€ λˆŒλ¦¬λŠ”λ° μ•ˆ
125
+ * μ—΄λ¦¬λŠ” λͺ©λ‘μ΄ λœλ‹€ β€” 선언은 있고 길이 μ—†λŠ” κ·Έ λͺ¨μ–‘이닀.
126
+ *
127
+ * 칸이 μ„œκΈ° 전에 λ‚œ μ„Έμ…˜μ—λŠ” 값이 μ—†λ‹€. κ·Έλ•ŒλŠ” μ§€κΈˆ λŒ€ν™”λ‘œ λ‘”λ‹€ β€” μ„œλ²„κ°€ λ‹€μŒ 턴에 μ§€κΈˆ λŒ€ν™”λ‘œ μ°λŠ”λ‹€.
128
+ *
129
+ * `undefined` λŠ” γ€Œκ·ΈλŒ€λ‘œ λ‘”λ‹€γ€λŠ” λœ»μ΄λ‹€.
130
+ */
131
+ export declare function followPickedConversation(pickedKind: string | undefined | null, currentKind: string | undefined): string | undefined;
@@ -77,4 +77,20 @@ export function pickInitialMode(current, choices, remembered) {
77
77
  return current;
78
78
  return choices.some(choice => choice.kind === remembered) ? remembered : current;
79
79
  }
80
+ /**
81
+ * λͺ©λ‘μ—μ„œ κ³ λ₯Έ λŒ€ν™”λ‘œ λ”°λΌκ°ˆ 것인가 β€” ADR-0072 κ²°μ • 3.
82
+ *
83
+ * λͺ©λ‘μ€ ν•œ λŒ€μƒμ˜ λŒ€ν™”λ₯Ό **λͺ¨λ‘** 보여 μ£Όκ³ , κ·Έ λŒ€μƒ μœ„μ—λŠ” λŒ€ν™”κ°€ λ‘˜ μ„€ 수 μžˆλ‹€(A17). κ³ λ₯Έ 것이 μ§€κΈˆ
84
+ * μ—΄λ¦° λŒ€ν™”μ™€ λ‹€λ₯Έλ° 화면이 κ·ΈλŒ€λ‘œ 있으면 문이 κ±°μ ˆν•˜κ³ (`session-kind-mismatch`), λͺ©λ‘μ€ λˆŒλ¦¬λŠ”λ° μ•ˆ
85
+ * μ—΄λ¦¬λŠ” λͺ©λ‘μ΄ λœλ‹€ β€” 선언은 있고 길이 μ—†λŠ” κ·Έ λͺ¨μ–‘이닀.
86
+ *
87
+ * 칸이 μ„œκΈ° 전에 λ‚œ μ„Έμ…˜μ—λŠ” 값이 μ—†λ‹€. κ·Έλ•ŒλŠ” μ§€κΈˆ λŒ€ν™”λ‘œ λ‘”λ‹€ β€” μ„œλ²„κ°€ λ‹€μŒ 턴에 μ§€κΈˆ λŒ€ν™”λ‘œ μ°λŠ”λ‹€.
88
+ *
89
+ * `undefined` λŠ” γ€Œκ·ΈλŒ€λ‘œ λ‘”λ‹€γ€λŠ” λœ»μ΄λ‹€.
90
+ */
91
+ export function followPickedConversation(pickedKind, currentKind) {
92
+ if (!pickedKind || pickedKind === currentKind)
93
+ return undefined;
94
+ return pickedKind;
95
+ }
80
96
  //# sourceMappingURL=assistant-mode.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-mode.js","sourceRoot":"","sources":["../../client/components/assistant-mode.ts"],"names":[],"mappings":"AAqDA,6FAA6F;AAC7F,MAAM,UAAU,uBAAuB,CAAC,CAAiB,EAAE,CAAiB;IAC1E,OAAO,GAAG,CAAC,EAAE,IAAI,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,IAAI,EAAE,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,IAAI,EAAE,EAAE,CAAA;AACxF,CAAC;AAoBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAA+B,EAC/B,GAAkC,EAClC,QAAuC,EACvC,IAAwB;IAExB,IAAI,IAAI,EAAE,CAAC;QACT,kFAAkF;QAClF,MAAM,OAAO,GAAG,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;QAClE,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,YAAY,EAAE,eAAe;YAC7B,cAAc,EAAE,OAAO,EAAE,cAAc,IAAI,EAAE;YAC7C,cAAc,EAAE,OAAO,EAAE,cAAc,IAAI,EAAE;YAC7C,eAAe,EAAE,OAAO,EAAE,eAAe,IAAI,EAAE;YAC/C,WAAW,EAAE,OAAO,EAAE,WAAW;YACjC,YAAY,EAAE,OAAO,EAAE,YAAY;YACnC,YAAY,EAAE,OAAO,EAAE,YAAY;SACpC,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,CAAuC,GAAM,EAA2B,EAAE,CACpF,GAAW,CAAC,GAAG,CAAC,IAAK,QAAgB,CAAC,GAAG,CAAC,CAAA;IAE7C,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,CAAC,CAAA;IACzC,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,uGAAuG;YACrG,qGAAqG;YACrG,qCAAqC,CACxC,CAAA;IACH,CAAC;IAED,OAAO;QACL,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC;QAChC,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC;QAChC,YAAY;QACZ,cAAc,EAAE,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE;QAC5C,cAAc,EAAE,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE;QAC5C,YAAY,EAAE,IAAI,CAAC,cAAc,CAAC;QAClC,eAAe,EAAE,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE;QAC9C,YAAY,EAAE,IAAI,CAAC,cAAc,CAAC;KACnC,CAAA;AACH,CAAC;AASD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAC7B,OAA2B,EAC3B,OAA2C,EAC3C,UAA8B;IAE9B,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,OAAO,CAAA;IACtC,IAAI,CAAC,UAAU,IAAI,UAAU,KAAK,OAAO;QAAE,OAAO,OAAO,CAAA;IACzD,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAA;AAClF,CAAC","sourcesContent":["/**\n * A mode is what a conversation *is*: which conversation this is, and what it is about.\n *\n * Those two used to be nine. The browser declared the persona, the tool categories, the model and\n * the limits and sent them with every turn, which meant a caller holding the door's privilege\n * chose its own authority β€” and it meant a screen could describe a conversation one way while the\n * server answered it another. Since ADR-0061 decision 7 the app server registers the mode and the\n * browser names it:\n *\n * ```ts\n * html`<ox-assistant-chat .mode=${{ kind: 'twin.space', target: spaceId, hostContext: { spaceId } }}></ox-assistant-chat>`\n * ```\n *\n * What the panel shows β€” the `/` sentences, the `@` catalog, the two notices β€” comes from the same\n * declaration, fetched from the server already written in the person's language, so the panel and\n * the door cannot offer different conversations.\n */\nexport interface AssistantMode {\n /**\n * Which conversation this is β€” a `<app>.<surface>` kind the app server registered.\n */\n kind: string\n\n /**\n * What it is about: the space, the station, the model being edited.\n *\n * **With `kind`, this is the conversation's lifetime.** When either changes the chat closes what\n * is on screen and starts again β€” a conversation about station 3 is not a conversation about\n * station 7, and leaving the old one up means the person reads an answer about a screen they are\n * no longer looking at.\n */\n target?: string\n\n /**\n * What the host knows about the subject; travels with each request.\n *\n * **The model never sees this** β€” it reaches the tools as `ctx.host` and nothing else (ADR-0061\n * decision 7). Anything the model must know is answered by a read tool.\n */\n hostContext?: any\n}\n\n/** What the server said this mode offers, already in the request's locale. */\nexport interface AssistantModeView {\n kind: string\n slashTemplates: any[]\n catalogEntries: any[]\n proposalChoices: any[]\n placeholder?: string\n footerNotice?: string\n stagedNotice?: string\n}\n\n/** True when these two describe different conversations, so the old one should be closed. */\nexport function isDifferentConversation(a?: AssistantMode, b?: AssistantMode): boolean {\n return `${a?.kind ?? ''}|${a?.target ?? ''}` !== `${b?.kind ?? ''}|${b?.target ?? ''}`\n}\n\n/** Everything a conversation needs, after the mode, the host's own properties, the registered\n * defaults and the server's answer have been reconciled. */\nexport interface ConversationSettings {\n /** Set when a mode names this conversation; absent for a host that declares none. */\n kind?: string\n target?: string\n hostContext?: any\n chatEndpoint: 'boardAIChat' | 'assistantChat'\n catalogEntries: any[]\n slashTemplates: any[]\n /** What the empty input says. A mode's comes translated from the server. */\n placeholder?: string\n /** Sentences, not keys β€” a mode's come translated from the server, a host's from its own i18n. */\n footerNotice?: string\n proposalChoices: any[]\n stagedNotice?: string\n}\n\n/**\n * Reconcile the places a setting can come from.\n *\n * **A host that declares a mode gets the mode and nothing else.** What the mode does not offer is\n * not filled in from the element's own properties or from the defaults another package registered.\n * Filling it back in is how the defect this type removes returned β€” board-ai registers board\n * authoring's `/add-monitor`, its component `@` catalog and its \"previewed on the board\" line\n * globally, and plant's factory mode, which declares no slash list, offered them on a factory\n * screen once the board modeller had been loaded.\n *\n * A mode's offering arrives asynchronously, so until the server has answered a mode offers\n * nothing. Standing the defaults in during that gap would show the board's list for a moment on a\n * screen that does not edit boards.\n *\n * A host that declares no mode gets exactly what it got before modes existed β€” its own properties,\n * falling back to the registered defaults β€” and it must name its own door: the `boardAIChat`\n * default was removed with decision 7, because a missing door should show when a surface is\n * declared, not when a person says something.\n */\nexport function resolveConversation(\n mode: AssistantMode | undefined,\n own: Partial<ConversationSettings>,\n defaults: Partial<ConversationSettings>,\n view?: AssistantModeView\n): ConversationSettings {\n if (mode) {\n /* A view for another kind is the previous conversation's answer arriving late. */\n const offered = view && view.kind === mode.kind ? view : undefined\n return {\n kind: mode.kind,\n target: mode.target,\n hostContext: mode.hostContext,\n chatEndpoint: 'assistantChat',\n catalogEntries: offered?.catalogEntries ?? [],\n slashTemplates: offered?.slashTemplates ?? [],\n proposalChoices: offered?.proposalChoices ?? [],\n placeholder: offered?.placeholder,\n footerNotice: offered?.footerNotice,\n stagedNotice: offered?.stagedNotice\n }\n }\n\n const pick = <K extends keyof ConversationSettings>(key: K): ConversationSettings[K] =>\n (own as any)[key] ?? (defaults as any)[key]\n\n const chatEndpoint = pick('chatEndpoint')\n if (!chatEndpoint) {\n throw new Error(\n '[ox-assistant-chat] a surface with no mode must name its chatEndpoint. The `boardAIChat` default was ' +\n 'removed (ADR-0061 decision 7) β€” apps declare a mode and talk to `assistantChat`; the board product ' +\n 'names `boardAIChat` on the element.'\n )\n }\n\n return {\n hostContext: pick('hostContext'),\n placeholder: pick('placeholder'),\n chatEndpoint,\n catalogEntries: pick('catalogEntries') ?? [],\n slashTemplates: pick('slashTemplates') ?? [],\n footerNotice: pick('footerNotice'),\n proposalChoices: pick('proposalChoices') ?? [],\n stagedNotice: pick('stagedNotice')\n }\n}\n\n/** ν•œ subject μœ„μ— μ„€ 수 μžˆλŠ” λŒ€ν™” ν•˜λ‚˜ β€” μ„œλ²„κ°€ μ€€ λͺ©λ‘μ˜ ν•œ 쀄. */\nexport interface AssistantModeChoice {\n kind: string\n label: string\n isDefault: boolean\n}\n\n/**\n * 이 화면을 μ—΄ λ•Œ μ–΄λŠ λŒ€ν™”λ‘œ μ„€ 것인가 β€” ADR-0072 A17 β‘ .\n *\n * ── μ™œ κΈ°μ–΅ν•˜λ‚˜ ───────────────────────────────────────────────────────────\n * λ³΄λ“œλ₯Ό μ—΄ λ•Œλ§ˆλ‹€ μ €μž‘ λŒ€ν™”λ‘œ λ˜λŒμ•„κ°€λ©΄, ν˜„μž₯을 물으렀고 온 μ‚¬λžŒμ€ 화면을 μ—΄ λ•Œλ§ˆλ‹€ 같은 μ†μ§ˆμ„\n * λ‹€μ‹œ ν•œλ‹€. 기얡은 이 λΈŒλΌμš°μ €μ—λ§Œ λ‚¨λŠ”λ‹€ β€” μ„œλ²„μ— 칸을 λ§Œλ“€μ§€ μ•ŠλŠ”λ‹€.\n *\n * ── 기얡을 μ–Έμ œ λ²„λ¦¬λ‚˜ ────────────────────────────────────────────────────\n * κΈ°μ–΅ν•œ μ’…λ₯˜κ°€ μ§€κΈˆ λͺ©λ‘μ— μ—†μœΌλ©΄ λ”°λ₯΄μ§€ μ•ŠλŠ”λ‹€. λŒ€ν™”κ°€ ν•˜λ‚˜ μ—†μ–΄μ§„ 판으둜 λ°”λ€Œμ—ˆλŠ”λ° κ·Έ μ΄λ¦„μœΌλ‘œ\n * μ—΄λ €κ³  ν•˜λ©΄ λŒ€ν™”κ°€ μ•ˆ 열리고, μ‚¬λžŒμ€ μžκΈ°κ°€ 무엇을 잘λͺ»ν–ˆλŠ”μ§€ μ•Œ 길이 μ—†λ‹€. κ·Έλ•ŒλŠ” host κ°€ μ€€\n * λŒ€ν™”λ‘œ λ‘”λ‹€.\n *\n * κ³ λ₯Ό 것이 ν•˜λ‚˜λΏμ΄λ©΄ 기얡을 보지 μ•ŠλŠ”λ‹€ β€” κ³ λ₯Έ 적이 μ—†λŠ” 화면이닀.\n */\nexport function pickInitialMode(\n current: string | undefined,\n choices: ReadonlyArray<AssistantModeChoice>,\n remembered: string | undefined\n): string | undefined {\n if (choices.length < 2) return current\n if (!remembered || remembered === current) return current\n return choices.some(choice => choice.kind === remembered) ? remembered : current\n}\n"]}
1
+ {"version":3,"file":"assistant-mode.js","sourceRoot":"","sources":["../../client/components/assistant-mode.ts"],"names":[],"mappings":"AAyDA,6FAA6F;AAC7F,MAAM,UAAU,uBAAuB,CAAC,CAAiB,EAAE,CAAiB;IAC1E,OAAO,GAAG,CAAC,EAAE,IAAI,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,IAAI,EAAE,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,IAAI,EAAE,EAAE,CAAA;AACxF,CAAC;AAoBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAA+B,EAC/B,GAAkC,EAClC,QAAuC,EACvC,IAAwB;IAExB,IAAI,IAAI,EAAE,CAAC;QACT,kFAAkF;QAClF,MAAM,OAAO,GAAG,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAA;QAClE,OAAO;YACL,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,YAAY,EAAE,eAAe;YAC7B,cAAc,EAAE,OAAO,EAAE,cAAc,IAAI,EAAE;YAC7C,cAAc,EAAE,OAAO,EAAE,cAAc,IAAI,EAAE;YAC7C,eAAe,EAAE,OAAO,EAAE,eAAe,IAAI,EAAE;YAC/C,WAAW,EAAE,OAAO,EAAE,WAAW;YACjC,YAAY,EAAE,OAAO,EAAE,YAAY;YACnC,YAAY,EAAE,OAAO,EAAE,YAAY;SACpC,CAAA;IACH,CAAC;IAED,MAAM,IAAI,GAAG,CAAuC,GAAM,EAA2B,EAAE,CACpF,GAAW,CAAC,GAAG,CAAC,IAAK,QAAgB,CAAC,GAAG,CAAC,CAAA;IAE7C,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,CAAC,CAAA;IACzC,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,uGAAuG;YACrG,qGAAqG;YACrG,qCAAqC,CACxC,CAAA;IACH,CAAC;IAED,OAAO;QACL,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC;QAChC,WAAW,EAAE,IAAI,CAAC,aAAa,CAAC;QAChC,YAAY;QACZ,cAAc,EAAE,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE;QAC5C,cAAc,EAAE,IAAI,CAAC,gBAAgB,CAAC,IAAI,EAAE;QAC5C,YAAY,EAAE,IAAI,CAAC,cAAc,CAAC;QAClC,eAAe,EAAE,IAAI,CAAC,iBAAiB,CAAC,IAAI,EAAE;QAC9C,YAAY,EAAE,IAAI,CAAC,cAAc,CAAC;KACnC,CAAA;AACH,CAAC;AASD;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAC7B,OAA2B,EAC3B,OAA2C,EAC3C,UAA8B;IAE9B,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,OAAO,CAAA;IACtC,IAAI,CAAC,UAAU,IAAI,UAAU,KAAK,OAAO;QAAE,OAAO,OAAO,CAAA;IACzD,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAA;AAClF,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,wBAAwB,CAAC,UAAqC,EAAE,WAA+B;IAC7G,IAAI,CAAC,UAAU,IAAI,UAAU,KAAK,WAAW;QAAE,OAAO,SAAS,CAAA;IAC/D,OAAO,UAAU,CAAA;AACnB,CAAC","sourcesContent":["/**\n * A mode is what a conversation *is*: which conversation this is, and what it is about.\n *\n * Those two used to be nine. The browser declared the persona, the tool categories, the model and\n * the limits and sent them with every turn, which meant a caller holding the door's privilege\n * chose its own authority β€” and it meant a screen could describe a conversation one way while the\n * server answered it another. Since ADR-0061 decision 7 the app server registers the mode and the\n * browser names it:\n *\n * ```ts\n * html`<ox-assistant-chat .mode=${{ kind: 'twin.space', target: spaceId, hostContext: { spaceId } }}></ox-assistant-chat>`\n * ```\n *\n * What the panel shows β€” the `/` sentences, the `@` catalog, the two notices β€” comes from the same\n * declaration, fetched from the server already written in the person's language, so the panel and\n * the door cannot offer different conversations.\n */\nexport interface AssistantMode {\n /**\n * Which conversation this is β€” a `<app>.<surface>` kind the app server registered.\n */\n kind: string\n\n /**\n * What it is about: the space, the station, the model being edited.\n *\n * **With `kind`, this is the conversation's lifetime.** When either changes the chat closes what\n * is on screen and starts again β€” a conversation about station 3 is not a conversation about\n * station 7, and leaving the old one up means the person reads an answer about a screen they are\n * no longer looking at.\n */\n target?: string\n\n /**\n * What the host knows about the subject; travels with each request.\n *\n * **The model never sees this** β€” it reaches the tools as `ctx.host` and nothing else (ADR-0061\n * decision 7). Anything the model must know is answered by a read tool.\n */\n hostContext?: any\n}\n\n/** What the server said this mode offers, already in the request's locale. */\nexport interface AssistantModeView {\n kind: string\n slashTemplates: any[]\n catalogEntries: any[]\n proposalChoices: any[]\n placeholder?: string\n /** 빈 λŒ€ν™”μ˜ 제λͺ©κ³Ό ν•œ 쀄 β€” 이 λŒ€ν™”κ°€ 무엇을 ν•˜λŠ” μžλ¦¬μΈμ§€. host 속성보닀 μ•žμ„ λ‹€. */\n intro?: { title: string; subtitle: string }\n /** 빈 λŒ€ν™”μ˜ μ˜ˆμ‹œ β€” 이 λŒ€ν™”μ—μ„œ μ‹€μ œλ‘œ ν•  수 μžˆλŠ” 말. host 속성보닀 μ•žμ„ λ‹€. */\n examples?: Array<{ label: string; items: string[] }>\n footerNotice?: string\n stagedNotice?: string\n}\n\n/** True when these two describe different conversations, so the old one should be closed. */\nexport function isDifferentConversation(a?: AssistantMode, b?: AssistantMode): boolean {\n return `${a?.kind ?? ''}|${a?.target ?? ''}` !== `${b?.kind ?? ''}|${b?.target ?? ''}`\n}\n\n/** Everything a conversation needs, after the mode, the host's own properties, the registered\n * defaults and the server's answer have been reconciled. */\nexport interface ConversationSettings {\n /** Set when a mode names this conversation; absent for a host that declares none. */\n kind?: string\n target?: string\n hostContext?: any\n chatEndpoint: 'boardAIChat' | 'assistantChat'\n catalogEntries: any[]\n slashTemplates: any[]\n /** What the empty input says. A mode's comes translated from the server. */\n placeholder?: string\n /** Sentences, not keys β€” a mode's come translated from the server, a host's from its own i18n. */\n footerNotice?: string\n proposalChoices: any[]\n stagedNotice?: string\n}\n\n/**\n * Reconcile the places a setting can come from.\n *\n * **A host that declares a mode gets the mode and nothing else.** What the mode does not offer is\n * not filled in from the element's own properties or from the defaults another package registered.\n * Filling it back in is how the defect this type removes returned β€” board-ai registers board\n * authoring's `/add-monitor`, its component `@` catalog and its \"previewed on the board\" line\n * globally, and plant's factory mode, which declares no slash list, offered them on a factory\n * screen once the board modeller had been loaded.\n *\n * A mode's offering arrives asynchronously, so until the server has answered a mode offers\n * nothing. Standing the defaults in during that gap would show the board's list for a moment on a\n * screen that does not edit boards.\n *\n * A host that declares no mode gets exactly what it got before modes existed β€” its own properties,\n * falling back to the registered defaults β€” and it must name its own door: the `boardAIChat`\n * default was removed with decision 7, because a missing door should show when a surface is\n * declared, not when a person says something.\n */\nexport function resolveConversation(\n mode: AssistantMode | undefined,\n own: Partial<ConversationSettings>,\n defaults: Partial<ConversationSettings>,\n view?: AssistantModeView\n): ConversationSettings {\n if (mode) {\n /* A view for another kind is the previous conversation's answer arriving late. */\n const offered = view && view.kind === mode.kind ? view : undefined\n return {\n kind: mode.kind,\n target: mode.target,\n hostContext: mode.hostContext,\n chatEndpoint: 'assistantChat',\n catalogEntries: offered?.catalogEntries ?? [],\n slashTemplates: offered?.slashTemplates ?? [],\n proposalChoices: offered?.proposalChoices ?? [],\n placeholder: offered?.placeholder,\n footerNotice: offered?.footerNotice,\n stagedNotice: offered?.stagedNotice\n }\n }\n\n const pick = <K extends keyof ConversationSettings>(key: K): ConversationSettings[K] =>\n (own as any)[key] ?? (defaults as any)[key]\n\n const chatEndpoint = pick('chatEndpoint')\n if (!chatEndpoint) {\n throw new Error(\n '[ox-assistant-chat] a surface with no mode must name its chatEndpoint. The `boardAIChat` default was ' +\n 'removed (ADR-0061 decision 7) β€” apps declare a mode and talk to `assistantChat`; the board product ' +\n 'names `boardAIChat` on the element.'\n )\n }\n\n return {\n hostContext: pick('hostContext'),\n placeholder: pick('placeholder'),\n chatEndpoint,\n catalogEntries: pick('catalogEntries') ?? [],\n slashTemplates: pick('slashTemplates') ?? [],\n footerNotice: pick('footerNotice'),\n proposalChoices: pick('proposalChoices') ?? [],\n stagedNotice: pick('stagedNotice')\n }\n}\n\n/** ν•œ subject μœ„μ— μ„€ 수 μžˆλŠ” λŒ€ν™” ν•˜λ‚˜ β€” μ„œλ²„κ°€ μ€€ λͺ©λ‘μ˜ ν•œ 쀄. */\nexport interface AssistantModeChoice {\n kind: string\n label: string\n isDefault: boolean\n}\n\n/**\n * 이 화면을 μ—΄ λ•Œ μ–΄λŠ λŒ€ν™”λ‘œ μ„€ 것인가 β€” ADR-0072 A17 β‘ .\n *\n * ── μ™œ κΈ°μ–΅ν•˜λ‚˜ ───────────────────────────────────────────────────────────\n * λ³΄λ“œλ₯Ό μ—΄ λ•Œλ§ˆλ‹€ μ €μž‘ λŒ€ν™”λ‘œ λ˜λŒμ•„κ°€λ©΄, ν˜„μž₯을 물으렀고 온 μ‚¬λžŒμ€ 화면을 μ—΄ λ•Œλ§ˆλ‹€ 같은 μ†μ§ˆμ„\n * λ‹€μ‹œ ν•œλ‹€. 기얡은 이 λΈŒλΌμš°μ €μ—λ§Œ λ‚¨λŠ”λ‹€ β€” μ„œλ²„μ— 칸을 λ§Œλ“€μ§€ μ•ŠλŠ”λ‹€.\n *\n * ── 기얡을 μ–Έμ œ λ²„λ¦¬λ‚˜ ────────────────────────────────────────────────────\n * κΈ°μ–΅ν•œ μ’…λ₯˜κ°€ μ§€κΈˆ λͺ©λ‘μ— μ—†μœΌλ©΄ λ”°λ₯΄μ§€ μ•ŠλŠ”λ‹€. λŒ€ν™”κ°€ ν•˜λ‚˜ μ—†μ–΄μ§„ 판으둜 λ°”λ€Œμ—ˆλŠ”λ° κ·Έ μ΄λ¦„μœΌλ‘œ\n * μ—΄λ €κ³  ν•˜λ©΄ λŒ€ν™”κ°€ μ•ˆ 열리고, μ‚¬λžŒμ€ μžκΈ°κ°€ 무엇을 잘λͺ»ν–ˆλŠ”μ§€ μ•Œ 길이 μ—†λ‹€. κ·Έλ•ŒλŠ” host κ°€ μ€€\n * λŒ€ν™”λ‘œ λ‘”λ‹€.\n *\n * κ³ λ₯Ό 것이 ν•˜λ‚˜λΏμ΄λ©΄ 기얡을 보지 μ•ŠλŠ”λ‹€ β€” κ³ λ₯Έ 적이 μ—†λŠ” 화면이닀.\n */\nexport function pickInitialMode(\n current: string | undefined,\n choices: ReadonlyArray<AssistantModeChoice>,\n remembered: string | undefined\n): string | undefined {\n if (choices.length < 2) return current\n if (!remembered || remembered === current) return current\n return choices.some(choice => choice.kind === remembered) ? remembered : current\n}\n\n/**\n * λͺ©λ‘μ—μ„œ κ³ λ₯Έ λŒ€ν™”λ‘œ λ”°λΌκ°ˆ 것인가 β€” ADR-0072 κ²°μ • 3.\n *\n * λͺ©λ‘μ€ ν•œ λŒ€μƒμ˜ λŒ€ν™”λ₯Ό **λͺ¨λ‘** 보여 μ£Όκ³ , κ·Έ λŒ€μƒ μœ„μ—λŠ” λŒ€ν™”κ°€ λ‘˜ μ„€ 수 μžˆλ‹€(A17). κ³ λ₯Έ 것이 μ§€κΈˆ\n * μ—΄λ¦° λŒ€ν™”μ™€ λ‹€λ₯Έλ° 화면이 κ·ΈλŒ€λ‘œ 있으면 문이 κ±°μ ˆν•˜κ³ (`session-kind-mismatch`), λͺ©λ‘μ€ λˆŒλ¦¬λŠ”λ° μ•ˆ\n * μ—΄λ¦¬λŠ” λͺ©λ‘μ΄ λœλ‹€ β€” 선언은 있고 길이 μ—†λŠ” κ·Έ λͺ¨μ–‘이닀.\n *\n * 칸이 μ„œκΈ° 전에 λ‚œ μ„Έμ…˜μ—λŠ” 값이 μ—†λ‹€. κ·Έλ•ŒλŠ” μ§€κΈˆ λŒ€ν™”λ‘œ λ‘”λ‹€ β€” μ„œλ²„κ°€ λ‹€μŒ 턴에 μ§€κΈˆ λŒ€ν™”λ‘œ μ°λŠ”λ‹€.\n *\n * `undefined` λŠ” γ€Œκ·ΈλŒ€λ‘œ λ‘”λ‹€γ€λŠ” λœ»μ΄λ‹€.\n */\nexport function followPickedConversation(pickedKind: string | undefined | null, currentKind: string | undefined): string | undefined {\n if (!pickedKind || pickedKind === currentKind) return undefined\n return pickedKind\n}\n"]}
@@ -9,7 +9,7 @@
9
9
  * the slash list was a module global. A mode's wording now comes from the same declaration the
10
10
  * door reads, so the two cannot describe different conversations.
11
11
  */
12
- import { isDifferentConversation, resolveConversation, pickInitialMode } from './assistant-mode';
12
+ import { isDifferentConversation, resolveConversation, pickInitialMode, followPickedConversation } from './assistant-mode';
13
13
  const MODE = { kind: 'twin.space', target: 'S-12', hostContext: { spaceId: 'S-12' } };
14
14
  /** What the server said this mode offers, already in the request's locale. */
15
15
  const MODE_VIEW = {
@@ -103,4 +103,20 @@ describe('pickInitialMode', () => {
103
103
  expect(pickInitialMode('twin.board', choices, undefined)).toBe('twin.board');
104
104
  });
105
105
  });
106
+ /*
107
+ * ADR-0072 κ²°μ • 3 β€” λͺ©λ‘μ—μ„œ κ³ λ₯Έ λŒ€ν™”λ‘œ 화면이 따라간닀.
108
+ */
109
+ describe('followPickedConversation', () => {
110
+ it('β˜… λ‹€λ₯Έ λŒ€ν™”μ˜ μ„Έμ…˜μ„ κ³ λ₯΄λ©΄ κ·Έ λŒ€ν™”λ‘œ κ°„λ‹€ β€” μ•ˆ 따라가면 문이 κ±°μ ˆν•œλ‹€', () => {
111
+ expect(followPickedConversation('figure.catalog', 'figure.modeller')).toBe('figure.catalog');
112
+ });
113
+ it('같은 λŒ€ν™”λ©΄ κ·ΈλŒ€λ‘œ λ‘”λ‹€', () => {
114
+ expect(followPickedConversation('figure.modeller', 'figure.modeller')).toBeUndefined();
115
+ });
116
+ it('β˜… 칸이 μ„œκΈ° μ „ μ„Έμ…˜μ€ μ§€κΈˆ λŒ€ν™”λ‘œ λ‘”λ‹€ β€” μ„œλ²„κ°€ λ‹€μŒ 턴에 μ°λŠ”λ‹€', () => {
117
+ expect(followPickedConversation(undefined, 'figure.modeller')).toBeUndefined();
118
+ expect(followPickedConversation(null, 'figure.modeller')).toBeUndefined();
119
+ expect(followPickedConversation('', 'figure.modeller')).toBeUndefined();
120
+ });
121
+ });
106
122
  //# sourceMappingURL=assistant-mode.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"assistant-mode.test.js","sourceRoot":"","sources":["../../client/components/assistant-mode.test.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A mode is one value, and the point of it is that the parts of a conversation cannot disagree\n * with each other. Since ADR-0061 decision 7 the parts live on the server: the browser names the\n * conversation (`kind`) and its subject (`target`), and the persona, the tool categories, the\n * model and the limits are the app server's.\n *\n * The defect this still guards against shipped: a screen whose prompt said \"you do not edit boards\n * here\" offered `/add-monitor` in the same panel, because the prompt was an element property and\n * the slash list was a module global. A mode's wording now comes from the same declaration the\n * door reads, so the two cannot describe different conversations.\n */\nimport { isDifferentConversation, resolveConversation, type AssistantMode, pickInitialMode } from './assistant-mode'\n\nconst MODE: AssistantMode = { kind: 'twin.space', target: 'S-12', hostContext: { spaceId: 'S-12' } }\n\n/** What the server said this mode offers, already in the request's locale. */\nconst MODE_VIEW = {\n kind: 'twin.space',\n slashTemplates: [{ name: 'status', description: 'summarize', template: '이 κ³΅κ°„μ˜ μ§€κΈˆ μƒνƒœλ₯Ό μš”μ•½ν•΄μ€˜' }],\n catalogEntries: [],\n proposalChoices: [],\n footerNotice: undefined,\n stagedNotice: undefined\n}\n\n/** The board product, which declares no mode and names its own door. */\nconst HOST_PROPERTIES = {\n hostContext: { boardId: 'B-1' },\n chatEndpoint: 'boardAIChat' as const\n}\n\nconst GLOBAL_DEFAULTS = {\n catalogEntries: [{ type: 'rect' }],\n slashTemplates: [{ name: 'add-monitor', template: 'λͺ¨λ‹ˆν„°λ₯Ό μΆ”κ°€ν•΄μ€˜' }],\n footerNotice: 'previewed on the board'\n}\n\ndescribe('a mode names the conversation; the server says what it offers', () => {\n test('β˜… the subject travels, and the door is the neutral one', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, MODE_VIEW)\n\n expect(c.kind).toBe('twin.space')\n expect(c.target).toBe('S-12')\n expect(c.hostContext).toEqual({ spaceId: 'S-12' })\n expect(c.chatEndpoint).toBe('assistantChat')\n })\n\n test('β˜… what the mode offers comes from the server, not from what another package registered', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, MODE_VIEW)\n\n expect(c.slashTemplates).toEqual(MODE_VIEW.slashTemplates)\n expect(JSON.stringify(c.slashTemplates)).not.toContain('add-monitor')\n expect(c.catalogEntries).toEqual([])\n expect(c.footerNotice).toBeUndefined()\n })\n\n test('β˜… before the server has answered, a mode offers nothing β€” the defaults do not stand in', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, undefined)\n\n expect(c.slashTemplates).toEqual([])\n expect(c.catalogEntries).toEqual([])\n })\n\n test('a view for another kind is ignored β€” the panel is showing this conversation', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, { ...MODE_VIEW, kind: 'twin.board' })\n\n expect(c.slashTemplates).toEqual([])\n })\n})\n\ndescribe('a host with no mode', () => {\n test('gets its own properties and the registered defaults, as before modes existed', () => {\n const c = resolveConversation(undefined, HOST_PROPERTIES, GLOBAL_DEFAULTS)\n\n expect(c.hostContext).toEqual({ boardId: 'B-1' })\n expect(c.slashTemplates).toEqual(GLOBAL_DEFAULTS.slashTemplates)\n expect(c.footerNotice).toBe('previewed on the board')\n expect(c.chatEndpoint).toBe('boardAIChat')\n })\n\n test('β˜… must name its door β€” there is no default any more', () => {\n expect(() => resolveConversation(undefined, {}, {})).toThrow(/chatEndpoint/)\n })\n})\n\ndescribe('isDifferentConversation', () => {\n const station3: AssistantMode = { kind: 'plant.station', target: '3' }\n\n test('β˜… a different subject ends the conversation, and so does a different kind', () => {\n expect(isDifferentConversation(station3, { kind: 'plant.station', target: '7' })).toBe(true)\n expect(isDifferentConversation(station3, { kind: 'plant.factory', target: '3' })).toBe(true)\n expect(isDifferentConversation(undefined, station3)).toBe(true)\n })\n\n test('the same subject, described again, does not', () => {\n expect(isDifferentConversation(station3, { ...station3, hostContext: { reread: true } })).toBe(false)\n expect(isDifferentConversation(undefined, undefined)).toBe(false)\n })\n})\n\n/*\n * ADR-0072 A17 β‘  β€” 화면을 μ—΄ λ•Œ μ–΄λŠ λŒ€ν™”λ‘œ μ„œλ‚˜.\n *\n * λ˜μ‚΄λ¦¬λŠ” 것은 κ³ λ₯Έ λŒ€ν™”μ΄μ§€ μ„Έμ…˜μ΄ μ•„λ‹ˆλ‹€. 그리고 μ—†μ–΄μ§„ λŒ€ν™”λ‘œ λ˜μ‚΄λ¦¬μ§€ μ•ŠλŠ”λ‹€ β€” 그러면 λŒ€ν™”κ°€\n * μ•ˆ 열리고 μ‚¬λžŒμ€ μžκΈ°κ°€ 무엇을 잘λͺ»ν–ˆλŠ”μ§€ μ•Œ 길이 μ—†λ‹€.\n */\ndescribe('pickInitialMode', () => {\n const choices = [\n { kind: 'twin.board', label: '이 κ·Έλ¦Ό 고치기', isDefault: true },\n { kind: 'twin.space', label: '이 ν˜„μž₯ μ§€κΈˆ', isDefault: false }\n ]\n\n it('β˜… μ§€λ‚œλ²ˆμ— κ³ λ₯Έ λŒ€ν™”λ‘œ λŒμ•„κ°„λ‹€ β€” 맀번 같은 μ†μ§ˆμ„ λ‹€μ‹œ ν•˜μ§€ μ•ŠλŠ”λ‹€', () => {\n expect(pickInitialMode('twin.board', choices, 'twin.space')).toBe('twin.space')\n })\n\n it('β˜… μ§€κΈˆ λͺ©λ‘μ— μ—†λŠ” 기얡은 λ”°λ₯΄μ§€ μ•ŠλŠ”λ‹€ β€” μ•ˆ μ—΄λ¦¬λŠ” λŒ€ν™”λ‘œ λ“€μ–΄κ°„λ‹€', () => {\n expect(pickInitialMode('twin.board', choices, 'twin.gone')).toBe('twin.board')\n })\n\n it('β˜… κ³ λ₯Ό 것이 ν•˜λ‚˜λΏμ΄λ©΄ 기얡을 보지 μ•ŠλŠ”λ‹€ β€” κ³ λ₯Έ 적이 μ—†λŠ” 화면이닀', () => {\n expect(pickInitialMode('plant.station', [choices[0]], 'twin.space')).toBe('plant.station')\n })\n\n it('기얡이 μ—†μœΌλ©΄ host κ°€ μ€€ κ²ƒμœΌλ‘œ μ„ λ‹€', () => {\n expect(pickInitialMode('twin.board', choices, undefined)).toBe('twin.board')\n })\n})\n"]}
1
+ {"version":3,"file":"assistant-mode.test.js","sourceRoot":"","sources":["../../client/components/assistant-mode.test.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * A mode is one value, and the point of it is that the parts of a conversation cannot disagree\n * with each other. Since ADR-0061 decision 7 the parts live on the server: the browser names the\n * conversation (`kind`) and its subject (`target`), and the persona, the tool categories, the\n * model and the limits are the app server's.\n *\n * The defect this still guards against shipped: a screen whose prompt said \"you do not edit boards\n * here\" offered `/add-monitor` in the same panel, because the prompt was an element property and\n * the slash list was a module global. A mode's wording now comes from the same declaration the\n * door reads, so the two cannot describe different conversations.\n */\nimport { isDifferentConversation, resolveConversation, type AssistantMode, pickInitialMode, followPickedConversation } from './assistant-mode'\n\nconst MODE: AssistantMode = { kind: 'twin.space', target: 'S-12', hostContext: { spaceId: 'S-12' } }\n\n/** What the server said this mode offers, already in the request's locale. */\nconst MODE_VIEW = {\n kind: 'twin.space',\n slashTemplates: [{ name: 'status', description: 'summarize', template: '이 κ³΅κ°„μ˜ μ§€κΈˆ μƒνƒœλ₯Ό μš”μ•½ν•΄μ€˜' }],\n catalogEntries: [],\n proposalChoices: [],\n footerNotice: undefined,\n stagedNotice: undefined\n}\n\n/** The board product, which declares no mode and names its own door. */\nconst HOST_PROPERTIES = {\n hostContext: { boardId: 'B-1' },\n chatEndpoint: 'boardAIChat' as const\n}\n\nconst GLOBAL_DEFAULTS = {\n catalogEntries: [{ type: 'rect' }],\n slashTemplates: [{ name: 'add-monitor', template: 'λͺ¨λ‹ˆν„°λ₯Ό μΆ”κ°€ν•΄μ€˜' }],\n footerNotice: 'previewed on the board'\n}\n\ndescribe('a mode names the conversation; the server says what it offers', () => {\n test('β˜… the subject travels, and the door is the neutral one', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, MODE_VIEW)\n\n expect(c.kind).toBe('twin.space')\n expect(c.target).toBe('S-12')\n expect(c.hostContext).toEqual({ spaceId: 'S-12' })\n expect(c.chatEndpoint).toBe('assistantChat')\n })\n\n test('β˜… what the mode offers comes from the server, not from what another package registered', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, MODE_VIEW)\n\n expect(c.slashTemplates).toEqual(MODE_VIEW.slashTemplates)\n expect(JSON.stringify(c.slashTemplates)).not.toContain('add-monitor')\n expect(c.catalogEntries).toEqual([])\n expect(c.footerNotice).toBeUndefined()\n })\n\n test('β˜… before the server has answered, a mode offers nothing β€” the defaults do not stand in', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, undefined)\n\n expect(c.slashTemplates).toEqual([])\n expect(c.catalogEntries).toEqual([])\n })\n\n test('a view for another kind is ignored β€” the panel is showing this conversation', () => {\n const c = resolveConversation(MODE, HOST_PROPERTIES, GLOBAL_DEFAULTS, { ...MODE_VIEW, kind: 'twin.board' })\n\n expect(c.slashTemplates).toEqual([])\n })\n})\n\ndescribe('a host with no mode', () => {\n test('gets its own properties and the registered defaults, as before modes existed', () => {\n const c = resolveConversation(undefined, HOST_PROPERTIES, GLOBAL_DEFAULTS)\n\n expect(c.hostContext).toEqual({ boardId: 'B-1' })\n expect(c.slashTemplates).toEqual(GLOBAL_DEFAULTS.slashTemplates)\n expect(c.footerNotice).toBe('previewed on the board')\n expect(c.chatEndpoint).toBe('boardAIChat')\n })\n\n test('β˜… must name its door β€” there is no default any more', () => {\n expect(() => resolveConversation(undefined, {}, {})).toThrow(/chatEndpoint/)\n })\n})\n\ndescribe('isDifferentConversation', () => {\n const station3: AssistantMode = { kind: 'plant.station', target: '3' }\n\n test('β˜… a different subject ends the conversation, and so does a different kind', () => {\n expect(isDifferentConversation(station3, { kind: 'plant.station', target: '7' })).toBe(true)\n expect(isDifferentConversation(station3, { kind: 'plant.factory', target: '3' })).toBe(true)\n expect(isDifferentConversation(undefined, station3)).toBe(true)\n })\n\n test('the same subject, described again, does not', () => {\n expect(isDifferentConversation(station3, { ...station3, hostContext: { reread: true } })).toBe(false)\n expect(isDifferentConversation(undefined, undefined)).toBe(false)\n })\n})\n\n/*\n * ADR-0072 A17 β‘  β€” 화면을 μ—΄ λ•Œ μ–΄λŠ λŒ€ν™”λ‘œ μ„œλ‚˜.\n *\n * λ˜μ‚΄λ¦¬λŠ” 것은 κ³ λ₯Έ λŒ€ν™”μ΄μ§€ μ„Έμ…˜μ΄ μ•„λ‹ˆλ‹€. 그리고 μ—†μ–΄μ§„ λŒ€ν™”λ‘œ λ˜μ‚΄λ¦¬μ§€ μ•ŠλŠ”λ‹€ β€” 그러면 λŒ€ν™”κ°€\n * μ•ˆ 열리고 μ‚¬λžŒμ€ μžκΈ°κ°€ 무엇을 잘λͺ»ν–ˆλŠ”μ§€ μ•Œ 길이 μ—†λ‹€.\n */\ndescribe('pickInitialMode', () => {\n const choices = [\n { kind: 'twin.board', label: '이 κ·Έλ¦Ό 고치기', isDefault: true },\n { kind: 'twin.space', label: '이 ν˜„μž₯ μ§€κΈˆ', isDefault: false }\n ]\n\n it('β˜… μ§€λ‚œλ²ˆμ— κ³ λ₯Έ λŒ€ν™”λ‘œ λŒμ•„κ°„λ‹€ β€” 맀번 같은 μ†μ§ˆμ„ λ‹€μ‹œ ν•˜μ§€ μ•ŠλŠ”λ‹€', () => {\n expect(pickInitialMode('twin.board', choices, 'twin.space')).toBe('twin.space')\n })\n\n it('β˜… μ§€κΈˆ λͺ©λ‘μ— μ—†λŠ” 기얡은 λ”°λ₯΄μ§€ μ•ŠλŠ”λ‹€ β€” μ•ˆ μ—΄λ¦¬λŠ” λŒ€ν™”λ‘œ λ“€μ–΄κ°„λ‹€', () => {\n expect(pickInitialMode('twin.board', choices, 'twin.gone')).toBe('twin.board')\n })\n\n it('β˜… κ³ λ₯Ό 것이 ν•˜λ‚˜λΏμ΄λ©΄ 기얡을 보지 μ•ŠλŠ”λ‹€ β€” κ³ λ₯Έ 적이 μ—†λŠ” 화면이닀', () => {\n expect(pickInitialMode('plant.station', [choices[0]], 'twin.space')).toBe('plant.station')\n })\n\n it('기얡이 μ—†μœΌλ©΄ host κ°€ μ€€ κ²ƒμœΌλ‘œ μ„ λ‹€', () => {\n expect(pickInitialMode('twin.board', choices, undefined)).toBe('twin.board')\n })\n})\n\n/*\n * ADR-0072 κ²°μ • 3 β€” λͺ©λ‘μ—μ„œ κ³ λ₯Έ λŒ€ν™”λ‘œ 화면이 따라간닀.\n */\ndescribe('followPickedConversation', () => {\n it('β˜… λ‹€λ₯Έ λŒ€ν™”μ˜ μ„Έμ…˜μ„ κ³ λ₯΄λ©΄ κ·Έ λŒ€ν™”λ‘œ κ°„λ‹€ β€” μ•ˆ 따라가면 문이 κ±°μ ˆν•œλ‹€', () => {\n expect(followPickedConversation('figure.catalog', 'figure.modeller')).toBe('figure.catalog')\n })\n\n it('같은 λŒ€ν™”λ©΄ κ·ΈλŒ€λ‘œ λ‘”λ‹€', () => {\n expect(followPickedConversation('figure.modeller', 'figure.modeller')).toBeUndefined()\n })\n\n it('β˜… 칸이 μ„œκΈ° μ „ μ„Έμ…˜μ€ μ§€κΈˆ λŒ€ν™”λ‘œ λ‘”λ‹€ β€” μ„œλ²„κ°€ λ‹€μŒ 턴에 μ°λŠ”λ‹€', () => {\n expect(followPickedConversation(undefined, 'figure.modeller')).toBeUndefined()\n expect(followPickedConversation(null, 'figure.modeller')).toBeUndefined()\n expect(followPickedConversation('', 'figure.modeller')).toBeUndefined()\n })\n})\n"]}
@@ -28,4 +28,22 @@ describe('the panel does not raise a second tab axis', () => {
28
28
  expect(bare('assistant-session-toolbar.ts')).toContain('role="tablist"');
29
29
  });
30
30
  });
31
+ /*
32
+ * μ²¨λΆ€λŠ” **λ˜λŠ” κ²ƒλ§Œ** κΆŒν•œλ‹€ β€” ADR-0071 보탬(2026-09-18).
33
+ *
34
+ * `.json` Β· `.txt` Β· `.csv` 도 λ°›κ³  μžˆμ—ˆλŠ”λ°, κ·Έ νŒŒμΌλ“€μ€ μ‹€λ €λ§Œ κ°€κ³  λͺ¨λΈμ— λΆ™λŠ” μ½”λ“œκ°€ μ—†μ—ˆλ‹€. μ‚¬λžŒμ΄
35
+ * CSV λ₯Ό 뢙이고 물으면 파일 칩이 뜨고 닡도 μ™”λ‹€ β€” κ·Έ 닡은 νŒŒμΌμ„ λ³Έ 적이 μ—†κ³  였λ₯˜λ„ μ•ˆ 났닀.
36
+ * μ΄λ―Έμ§€λŠ” `hostContext.image` 둜 μ‚¬λžŒ 턴에 λΆ™λŠ” 길이 μžˆλ‹€.
37
+ */
38
+ describe('the attach control offers only what has a path', () => {
39
+ it('β˜… μ΄λ―Έμ§€λ§Œ λ°›λŠ”λ‹€ β€” μ½νžˆμ§€ μ•ŠλŠ” 것을 화면이 κΆŒν•˜μ§€ μ•ŠλŠ”λ‹€', () => {
40
+ /*
41
+ * μ—¬κΈ°μ„œλŠ” 주석을 μ•ˆ λ²—κΈ΄λ‹€. `image/*` μžμ²΄κ°€ `/*` λ₯Ό ν’ˆκ³  μžˆμ–΄μ„œ, 주석 λ²—κΈ°κ°œκ°€ κ·Έ 자리λ₯Ό μ£Όμ„μ˜
42
+ * μ‹œμž‘μœΌλ‘œ 읽고 λ‹€μŒ `*` + `/` κΉŒμ§€ μ§€μ›Œ 버린닀 β€” 값이 사라지고 μ‹œν—˜μ€ γ€Œμ—†λ‹€γ€κ³  λ‹΅ν•œλ‹€.
43
+ * 이 값은 주석에 적힐 일이 μ—†μœΌλ―€λ‘œ μ›λ¬Έμ—μ„œ κ·ΈλŒ€λ‘œ μ½λŠ”λ‹€.
44
+ */
45
+ const accept = readFileSync(join(__dirname, 'assistant-chat.ts'), 'utf-8').match(/accept="([^"]*)"/);
46
+ expect(accept?.[1]).toBe('image/*');
47
+ });
48
+ });
31
49
  //# sourceMappingURL=panel-aria.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"panel-aria.test.js","sourceRoot":"","sources":["../../client/components/panel-aria.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,SAAS,IAAI,CAAC,IAAY;IACxB,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC;SAChD,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC;SAChC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAA;AACjC,CAAC;AAED,QAAQ,CAAC,4CAA4C,EAAE,GAAG,EAAE;IAC1D,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,mBAAmB,CAAC,CAAC,QAAQ,CAAC,uBAAuB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAE7F,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAC3B,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAA;IAC1E,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["/*\n * νŒ¨λ„ μ•ˆμ— κ³ λ₯΄λŠ” 좕이 λ‘˜μΌ λ•Œ β€” λ‘˜ λ‹€ γ€Œνƒ­γ€μ΄λΌκ³  λ§ν•˜μ§€ μ•ŠλŠ”λ‹€.\n *\n * ── μ™œ 이 μ‹œν—˜μ΄ μžˆλ‚˜ ──────────────────────────────────────────────────────\n * λŒ€ν™” κ³ λ₯΄κ°œλ₯Ό μΉ© μ€„λ‘œ λ§Œλ“€λ©΄μ„œ `role=\"tablist\"` λ₯Ό λ‹¬μ•˜λ‹€. λ°”λ‘œ μ•„λž˜κ°€ μ„Έμ…˜ 탭이고 κ·Έμͺ½λ„ tablist\n * λΌμ„œ, ν™”λ©΄ λ‚­λ…κΈ°λ‘œ λ“€μœΌλ©΄ γ€Œνƒ­ λͺ©λ‘, λŒ€ν™”γ€ λ‹€μŒμ— γ€Œνƒ­ λͺ©λ‘, Conversation history」 κ°€ μ΄μ–΄μ‘Œλ‹€.\n * κ³ λ₯΄λŠ” 좕이 λ‘˜μΈλ° λ‘˜ λ‹€ 같은 μ΄λ¦„μœΌλ‘œ λΆˆλ¦°λ‹€. 눈으둜 보면 λ©€μ©‘ν•΄μ„œ ν™”λ©΄μ—μ„œλŠ” μ•ˆ μž‘νžŒλ‹€ β€”\n * UX 레인이 μ†ŒμŠ€λ₯Ό 읽고 μž‘μ•˜λ‹€(2026-09-18).\n *\n * νƒ­ 좕은 μ„Έμ…˜ toolbar(`assistant-session-toolbar.ts`)κ°€ κ°–λŠ”λ‹€. 이 νŒ¨λ„μ€ 자기 νƒ­ 좕을 μ„Έμš°μ§€ μ•ŠλŠ”λ‹€.\n * ARIA 탭은 쒌우 ν™”μ‚΄ν‘œ 이동과 `tabpanel` 이 ν•¨κ»˜ μžˆμ–΄μ•Ό μ„±λ¦½ν•˜λŠ”λ°, μ—¬κΈ°μ—λŠ” λ‘˜ λ‹€ μ—†λ‹€.\n *\n * 주석은 벗겨 λ‚Έλ‹€ β€” μœ„ 문단에 적힌 `role=\"tablist\"` κ°€ 검사λ₯Ό ν†΅κ³Όμ‹œν‚€λ©΄ κ·Έ κ²€μ‚¬λŠ” 자기 산문을 μ„Όλ‹€.\n */\nimport { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nfunction bare(name: string): string {\n return readFileSync(join(__dirname, name), 'utf-8')\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .replace(/^\\s*\\/\\/.*$/gm, '')\n}\n\ndescribe('the panel does not raise a second tab axis', () => {\n it('β˜… assistant-chat λŠ” tablist/tab 을 μ„ μ–Έν•˜μ§€ μ•ŠλŠ”λ‹€ β€” κ·Έ 좕은 μ„Έμ…˜ toolbar의 것이닀', () => {\n const found = [...bare('assistant-chat.ts').matchAll(/role=\"(tablist|tab)\"/g)].map(m => m[1])\n\n expect(found).toEqual([])\n })\n\n it('μ„Έμ…˜ toolbarλŠ” κ·Έ 좕을 계속 κ°–λŠ”λ‹€ β€” μœ„ μ‹œν—˜μ΄ 좕을 μ—†μ• λΌλŠ” 뜻으둜 μ½νžˆμ§€ μ•Šλ„λ‘', () => {\n expect(bare('assistant-session-toolbar.ts')).toContain('role=\"tablist\"')\n })\n})\n"]}
1
+ {"version":3,"file":"panel-aria.test.js","sourceRoot":"","sources":["../../client/components/panel-aria.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,SAAS,IAAI,CAAC,IAAY;IACxB,OAAO,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC;SAChD,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC;SAChC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAA;AACjC,CAAC;AAED,QAAQ,CAAC,4CAA4C,EAAE,GAAG,EAAE;IAC1D,EAAE,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC1E,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,mBAAmB,CAAC,CAAC,QAAQ,CAAC,uBAAuB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;QAE7F,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAC3B,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC,CAAA;IAC1E,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF;;;;;;GAMG;AACH,QAAQ,CAAC,gDAAgD,EAAE,GAAG,EAAE;IAC9D,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C;;;;WAIG;QACH,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,mBAAmB,CAAC,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAA;QAEpG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IACrC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["/*\n * νŒ¨λ„ μ•ˆμ— κ³ λ₯΄λŠ” 좕이 λ‘˜μΌ λ•Œ β€” λ‘˜ λ‹€ γ€Œνƒ­γ€μ΄λΌκ³  λ§ν•˜μ§€ μ•ŠλŠ”λ‹€.\n *\n * ── μ™œ 이 μ‹œν—˜μ΄ μžˆλ‚˜ ──────────────────────────────────────────────────────\n * λŒ€ν™” κ³ λ₯΄κ°œλ₯Ό μΉ© μ€„λ‘œ λ§Œλ“€λ©΄μ„œ `role=\"tablist\"` λ₯Ό λ‹¬μ•˜λ‹€. λ°”λ‘œ μ•„λž˜κ°€ μ„Έμ…˜ 탭이고 κ·Έμͺ½λ„ tablist\n * λΌμ„œ, ν™”λ©΄ λ‚­λ…κΈ°λ‘œ λ“€μœΌλ©΄ γ€Œνƒ­ λͺ©λ‘, λŒ€ν™”γ€ λ‹€μŒμ— γ€Œνƒ­ λͺ©λ‘, Conversation history」 κ°€ μ΄μ–΄μ‘Œλ‹€.\n * κ³ λ₯΄λŠ” 좕이 λ‘˜μΈλ° λ‘˜ λ‹€ 같은 μ΄λ¦„μœΌλ‘œ λΆˆλ¦°λ‹€. 눈으둜 보면 λ©€μ©‘ν•΄μ„œ ν™”λ©΄μ—μ„œλŠ” μ•ˆ μž‘νžŒλ‹€ β€”\n * UX 레인이 μ†ŒμŠ€λ₯Ό 읽고 μž‘μ•˜λ‹€(2026-09-18).\n *\n * νƒ­ 좕은 μ„Έμ…˜ toolbar(`assistant-session-toolbar.ts`)κ°€ κ°–λŠ”λ‹€. 이 νŒ¨λ„μ€ 자기 νƒ­ 좕을 μ„Έμš°μ§€ μ•ŠλŠ”λ‹€.\n * ARIA 탭은 쒌우 ν™”μ‚΄ν‘œ 이동과 `tabpanel` 이 ν•¨κ»˜ μžˆμ–΄μ•Ό μ„±λ¦½ν•˜λŠ”λ°, μ—¬κΈ°μ—λŠ” λ‘˜ λ‹€ μ—†λ‹€.\n *\n * 주석은 벗겨 λ‚Έλ‹€ β€” μœ„ 문단에 적힌 `role=\"tablist\"` κ°€ 검사λ₯Ό ν†΅κ³Όμ‹œν‚€λ©΄ κ·Έ κ²€μ‚¬λŠ” 자기 산문을 μ„Όλ‹€.\n */\nimport { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nfunction bare(name: string): string {\n return readFileSync(join(__dirname, name), 'utf-8')\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .replace(/^\\s*\\/\\/.*$/gm, '')\n}\n\ndescribe('the panel does not raise a second tab axis', () => {\n it('β˜… assistant-chat λŠ” tablist/tab 을 μ„ μ–Έν•˜μ§€ μ•ŠλŠ”λ‹€ β€” κ·Έ 좕은 μ„Έμ…˜ toolbar의 것이닀', () => {\n const found = [...bare('assistant-chat.ts').matchAll(/role=\"(tablist|tab)\"/g)].map(m => m[1])\n\n expect(found).toEqual([])\n })\n\n it('μ„Έμ…˜ toolbarλŠ” κ·Έ 좕을 계속 κ°–λŠ”λ‹€ β€” μœ„ μ‹œν—˜μ΄ 좕을 μ—†μ• λΌλŠ” 뜻으둜 μ½νžˆμ§€ μ•Šλ„λ‘', () => {\n expect(bare('assistant-session-toolbar.ts')).toContain('role=\"tablist\"')\n })\n})\n\n/*\n * μ²¨λΆ€λŠ” **λ˜λŠ” κ²ƒλ§Œ** κΆŒν•œλ‹€ β€” ADR-0071 보탬(2026-09-18).\n *\n * `.json` Β· `.txt` Β· `.csv` 도 λ°›κ³  μžˆμ—ˆλŠ”λ°, κ·Έ νŒŒμΌλ“€μ€ μ‹€λ €λ§Œ κ°€κ³  λͺ¨λΈμ— λΆ™λŠ” μ½”λ“œκ°€ μ—†μ—ˆλ‹€. μ‚¬λžŒμ΄\n * CSV λ₯Ό 뢙이고 물으면 파일 칩이 뜨고 닡도 μ™”λ‹€ β€” κ·Έ 닡은 νŒŒμΌμ„ λ³Έ 적이 μ—†κ³  였λ₯˜λ„ μ•ˆ 났닀.\n * μ΄λ―Έμ§€λŠ” `hostContext.image` 둜 μ‚¬λžŒ 턴에 λΆ™λŠ” 길이 μžˆλ‹€.\n */\ndescribe('the attach control offers only what has a path', () => {\n it('β˜… μ΄λ―Έμ§€λ§Œ λ°›λŠ”λ‹€ β€” μ½νžˆμ§€ μ•ŠλŠ” 것을 화면이 κΆŒν•˜μ§€ μ•ŠλŠ”λ‹€', () => {\n /*\n * μ—¬κΈ°μ„œλŠ” 주석을 μ•ˆ λ²—κΈ΄λ‹€. `image/*` μžμ²΄κ°€ `/*` λ₯Ό ν’ˆκ³  μžˆμ–΄μ„œ, 주석 λ²—κΈ°κ°œκ°€ κ·Έ 자리λ₯Ό μ£Όμ„μ˜\n * μ‹œμž‘μœΌλ‘œ 읽고 λ‹€μŒ `*` + `/` κΉŒμ§€ μ§€μ›Œ 버린닀 β€” 값이 사라지고 μ‹œν—˜μ€ γ€Œμ—†λ‹€γ€κ³  λ‹΅ν•œλ‹€.\n * 이 값은 주석에 적힐 일이 μ—†μœΌλ―€λ‘œ μ›λ¬Έμ—μ„œ κ·ΈλŒ€λ‘œ μ½λŠ”λ‹€.\n */\n const accept = readFileSync(join(__dirname, 'assistant-chat.ts'), 'utf-8').match(/accept=\"([^\"]*)\"/)\n\n expect(accept?.[1]).toBe('image/*')\n })\n})\n"]}