@things-factory/ai-assistant 10.1.88 → 10.1.91

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 (93) hide show
  1. package/client/components/assistant-chat.ts +45 -5
  2. package/client/components/assistant-dock.ts +30 -2
  3. package/client/components/entry-review.ts +246 -0
  4. package/client/components/workspace-view.ts +428 -0
  5. package/client/pages/assistant-workspace-page.ts +237 -0
  6. package/client/route.ts +9 -0
  7. package/client/utils/assistant-proposals.ts +11 -1
  8. package/client/utils/entry-proposal.ts +103 -0
  9. package/client/utils/workspace.ts +89 -0
  10. package/dist-client/components/assistant-chat.d.ts +3 -0
  11. package/dist-client/components/assistant-chat.js +46 -5
  12. package/dist-client/components/assistant-chat.js.map +1 -1
  13. package/dist-client/components/assistant-dock.d.ts +1 -0
  14. package/dist-client/components/assistant-dock.js +25 -2
  15. package/dist-client/components/assistant-dock.js.map +1 -1
  16. package/dist-client/components/entry-review.d.ts +9 -0
  17. package/dist-client/components/entry-review.js +247 -0
  18. package/dist-client/components/entry-review.js.map +1 -0
  19. package/dist-client/components/workspace-view.d.ts +19 -0
  20. package/dist-client/components/workspace-view.js +416 -0
  21. package/dist-client/components/workspace-view.js.map +1 -0
  22. package/dist-client/pages/assistant-workspace-page.d.ts +27 -0
  23. package/dist-client/pages/assistant-workspace-page.js +263 -0
  24. package/dist-client/pages/assistant-workspace-page.js.map +1 -0
  25. package/dist-client/route.d.ts +1 -0
  26. package/dist-client/route.js +10 -0
  27. package/dist-client/route.js.map +1 -0
  28. package/dist-client/tsconfig.tsbuildinfo +1 -1
  29. package/dist-client/utils/assistant-proposals.d.ts +1 -0
  30. package/dist-client/utils/assistant-proposals.js +9 -1
  31. package/dist-client/utils/assistant-proposals.js.map +1 -1
  32. package/dist-client/utils/entry-proposal.d.ts +55 -0
  33. package/dist-client/utils/entry-proposal.js +52 -0
  34. package/dist-client/utils/entry-proposal.js.map +1 -0
  35. package/dist-client/utils/workspace.d.ts +40 -0
  36. package/dist-client/utils/workspace.js +68 -0
  37. package/dist-client/utils/workspace.js.map +1 -0
  38. package/dist-server/service/assistant-mode/registry.js +13 -0
  39. package/dist-server/service/assistant-mode/registry.js.map +1 -1
  40. package/dist-server/service/assistant-mode/resolve.d.ts +11 -0
  41. package/dist-server/service/assistant-mode/resolve.js +15 -3
  42. package/dist-server/service/assistant-mode/resolve.js.map +1 -1
  43. package/dist-server/service/assistant-mode/types.d.ts +7 -1
  44. package/dist-server/service/assistant-mode/types.js.map +1 -1
  45. package/dist-server/service/data-entry/entry-apply.d.ts +46 -0
  46. package/dist-server/service/data-entry/entry-apply.js +102 -0
  47. package/dist-server/service/data-entry/entry-apply.js.map +1 -0
  48. package/dist-server/service/data-entry/entry-resolver.d.ts +4 -0
  49. package/dist-server/service/data-entry/entry-resolver.js +80 -0
  50. package/dist-server/service/data-entry/entry-resolver.js.map +1 -0
  51. package/dist-server/service/data-entry/entry-sources.d.ts +1 -0
  52. package/dist-server/service/data-entry/entry-sources.js +41 -0
  53. package/dist-server/service/data-entry/entry-sources.js.map +1 -0
  54. package/dist-server/service/data-entry/entry-target.d.ts +157 -0
  55. package/dist-server/service/data-entry/entry-target.js +343 -0
  56. package/dist-server/service/data-entry/entry-target.js.map +1 -0
  57. package/dist-server/service/data-entry/entry-tools.d.ts +43 -0
  58. package/dist-server/service/data-entry/entry-tools.js +185 -0
  59. package/dist-server/service/data-entry/entry-tools.js.map +1 -0
  60. package/dist-server/service/index.d.ts +6 -1
  61. package/dist-server/service/index.js +13 -2
  62. package/dist-server/service/index.js.map +1 -1
  63. package/dist-server/service/workspace/workspace-blocks.d.ts +24 -0
  64. package/dist-server/service/workspace/workspace-blocks.js +118 -0
  65. package/dist-server/service/workspace/workspace-blocks.js.map +1 -0
  66. package/dist-server/service/workspace/workspace-tools.d.ts +24 -0
  67. package/dist-server/service/workspace/workspace-tools.js +65 -0
  68. package/dist-server/service/workspace/workspace-tools.js.map +1 -0
  69. package/dist-server/tsconfig.tsbuildinfo +1 -1
  70. package/package.json +2 -2
  71. package/server/service/assistant-mode/registry.ts +13 -0
  72. package/server/service/assistant-mode/resolve.test.ts +2 -1
  73. package/server/service/assistant-mode/resolve.ts +15 -3
  74. package/server/service/assistant-mode/types.ts +7 -1
  75. package/server/service/data-entry/data-entry.test.ts +311 -0
  76. package/server/service/data-entry/entry-apply.ts +134 -0
  77. package/server/service/data-entry/entry-resolver.ts +74 -0
  78. package/server/service/data-entry/entry-sources.ts +35 -0
  79. package/server/service/data-entry/entry-target.ts +442 -0
  80. package/server/service/data-entry/entry-tools.ts +174 -0
  81. package/server/service/index.ts +10 -1
  82. package/server/service/workspace/workspace-blocks.ts +127 -0
  83. package/server/service/workspace/workspace-tools.ts +66 -0
  84. package/server/service/workspace/workspace.test.ts +58 -0
  85. package/test/entry-proposal.test.ts +67 -0
  86. package/test/open-doors.test.ts +2 -1
  87. package/test/workspace.test.ts +54 -0
  88. package/things-factory.config.js +6 -1
  89. package/translations/en.json +56 -1
  90. package/translations/ja.json +56 -1
  91. package/translations/ko.json +56 -1
  92. package/translations/ms.json +56 -1
  93. package/translations/zh.json +56 -1
@@ -25,5 +25,6 @@ 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
+ export declare function setEntryProposalHandler(handler: ProposalHandler): void;
28
29
  /** Runs the proposal - never throws; a failure is a `refused` outcome with its message as the code. */
29
30
  export declare function runProposal(proposal: any, context: ProposalContext): Promise<ProposalOutcome>;
@@ -6,9 +6,17 @@ export function registerProposalHandler(tool, handler) {
6
6
  export function proposalHandlerOf(tool) {
7
7
  return tool ? handlers.get(tool) : undefined;
8
8
  }
9
+ /*
10
+ * Data entry cards are the framework's, whatever record they enter (design/ai/data-entry.md §5): one
11
+ * handler asks the server to enter the stored rows. The dock sets it; no app registers one per record.
12
+ */
13
+ let entryHandler;
14
+ export function setEntryProposalHandler(handler) {
15
+ entryHandler = handler;
16
+ }
9
17
  /** Runs the proposal - never throws; a failure is a `refused` outcome with its message as the code. */
10
18
  export async function runProposal(proposal, context) {
11
- const handler = proposalHandlerOf(proposal?.tool);
19
+ const handler = proposal?.kind === 'entry' && entryHandler ? entryHandler : proposalHandlerOf(proposal?.tool);
12
20
  if (!handler)
13
21
  return { verdict: 'unavailable', errorCode: 'unknown-proposal-kind' };
14
22
  try {
@@ -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,uGAAuG;AACvG,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,QAAa,EAAE,OAAwB;IACvE,MAAM,OAAO,GAAG,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IACjD,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/** 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 = 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;;;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"]}
@@ -0,0 +1,55 @@
1
+ import type { ProposalContext, ProposalOutcome } from './assistant-proposals.js';
2
+ export interface EntryIssueView {
3
+ field?: string;
4
+ code: string;
5
+ messageKey?: string;
6
+ params?: Record<string, unknown>;
7
+ blocking: boolean;
8
+ }
9
+ export interface EntryRowView {
10
+ index: number;
11
+ values: Record<string, unknown>;
12
+ sources: {
13
+ attachmentId?: string;
14
+ where?: string;
15
+ }[];
16
+ issues: EntryIssueView[];
17
+ /** The server found this row's numbers and dates together on this line of the cited file. */
18
+ evidence?: {
19
+ attachmentId: string;
20
+ line: number;
21
+ };
22
+ }
23
+ export interface EntryTotalView {
24
+ field: string;
25
+ all: number;
26
+ ready: number;
27
+ declared?: number;
28
+ declaredFound?: boolean;
29
+ difference?: number;
30
+ }
31
+ export interface EntryRowResult {
32
+ index: number;
33
+ ok: boolean;
34
+ id?: string;
35
+ label?: string;
36
+ error?: string;
37
+ }
38
+ export interface EntryApplyAnswer extends ProposalOutcome {
39
+ rows?: EntryRowResult[];
40
+ }
41
+ export declare function isEntryProposal(proposal: any): boolean;
42
+ export declare function entryReadyCount(proposal: any): number;
43
+ /** The i18n key of an issue - the target's own, or the framework's by code. */
44
+ export declare function entryIssueKey(issue: EntryIssueView): string;
45
+ /** The issues of one cell, and of the row as a whole (no field). */
46
+ export declare function entryCellIssues(row: EntryRowView, field?: string): EntryIssueView[];
47
+ /** What stays in the conversation - the model reads it on its next turn, and the person sees what went in. */
48
+ export declare function entryOutcomeNote(answer: EntryApplyAnswer, t: (key: string, options?: any) => string): string;
49
+ /** The handler every entry card runs - built with the mutation so the rules are tested without a server. */
50
+ export declare function entryProposalHandler(apply: (sessionId: string, proposalKey: string) => Promise<EntryApplyAnswer>, t: (key: string, options?: any) => string): (proposal: any, context: ProposalContext) => Promise<ProposalOutcome>;
51
+ /**
52
+ * The newest card per record, over the conversation's proposals in order - an older one is replaced and
53
+ * cannot be entered (the server refuses it as `superseded`; the card says so before anyone presses).
54
+ */
55
+ export declare function latestEntryKeys(proposalsInOrder: any[][]): Map<string, string>;
@@ -0,0 +1,52 @@
1
+ export function isEntryProposal(proposal) {
2
+ return proposal?.kind === 'entry' && Array.isArray(proposal?.rows);
3
+ }
4
+ export function entryReadyCount(proposal) {
5
+ return (proposal?.rows ?? []).filter((row) => !row.issues?.some(issue => issue.blocking)).length;
6
+ }
7
+ /** The i18n key of an issue - the target's own, or the framework's by code. */
8
+ export function entryIssueKey(issue) {
9
+ return issue.messageKey || `ai-assistant.entry.issue.${issue.code}`;
10
+ }
11
+ /** The issues of one cell, and of the row as a whole (no field). */
12
+ export function entryCellIssues(row, field) {
13
+ return (row.issues ?? []).filter(issue => (field ? issue.field === field : !issue.field));
14
+ }
15
+ /** What stays in the conversation - the model reads it on its next turn, and the person sees what went in. */
16
+ export function entryOutcomeNote(answer, t) {
17
+ const refused = (answer.rows ?? []).filter(row => !row.ok);
18
+ const head = answer.applied
19
+ ? t('ai-assistant.entry.entered', { defaultValue: 'entered {count} rows.', count: answer.applied })
20
+ : t('ai-assistant.entry.entered-none', { defaultValue: 'no row was entered.' });
21
+ const lines = refused.slice(0, 20).map(row => t('ai-assistant.entry.row-refused', { defaultValue: 'row {n}: {reason}', n: row.index + 1, reason: row.error || '' }));
22
+ const more = refused.length > 20 ? [t('ai-assistant.entry.more-refused', { defaultValue: '{count} more rows were not entered.', count: refused.length - 20 })] : [];
23
+ return [head, ...lines, ...more].join('\n');
24
+ }
25
+ /** The handler every entry card runs - built with the mutation so the rules are tested without a server. */
26
+ export function entryProposalHandler(apply, t) {
27
+ return async (proposal, context) => {
28
+ if (!context.sessionId || !proposal?.key)
29
+ return { verdict: 'unavailable', errorCode: 'no-conversation' };
30
+ const answer = await apply(context.sessionId, proposal.key);
31
+ return {
32
+ verdict: answer.verdict,
33
+ ...(answer.errorCode ? { errorCode: answer.errorCode } : {}),
34
+ applied: answer.applied ?? 0,
35
+ missed: answer.missed ?? 0,
36
+ note: entryOutcomeNote(answer, t)
37
+ };
38
+ };
39
+ }
40
+ /**
41
+ * The newest card per record, over the conversation's proposals in order - an older one is replaced and
42
+ * cannot be entered (the server refuses it as `superseded`; the card says so before anyone presses).
43
+ */
44
+ export function latestEntryKeys(proposalsInOrder) {
45
+ const latest = new Map();
46
+ for (const list of proposalsInOrder)
47
+ for (const one of list ?? [])
48
+ if (isEntryProposal(one) && one.key)
49
+ latest.set(one.target, one.key);
50
+ return latest;
51
+ }
52
+ //# sourceMappingURL=entry-proposal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry-proposal.js","sourceRoot":"","sources":["../../client/utils/entry-proposal.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The data entry card's rules and its handler (operato-application/design/ai/data-entry.md §5).\n *\n * The card is a review, not an editor: a correction is said in the conversation and the model proposes\n * again - the newer card replaces the older one. Pressing asks the server to enter the stored rows; the\n * screen sends no rows of its own.\n */\nimport type { ProposalContext, ProposalOutcome } from './assistant-proposals.js'\n\nexport interface EntryIssueView {\n field?: string\n code: string\n messageKey?: string\n params?: Record<string, unknown>\n blocking: boolean\n}\n\nexport interface EntryRowView {\n index: number\n values: Record<string, unknown>\n sources: { attachmentId?: string; where?: string }[]\n issues: EntryIssueView[]\n /** The server found this row's numbers and dates together on this line of the cited file. */\n evidence?: { attachmentId: string; line: number }\n}\n\nexport interface EntryTotalView {\n field: string\n all: number\n ready: number\n declared?: number\n declaredFound?: boolean\n difference?: number\n}\n\nexport interface EntryRowResult {\n index: number\n ok: boolean\n id?: string\n label?: string\n error?: string\n}\n\nexport interface EntryApplyAnswer extends ProposalOutcome {\n rows?: EntryRowResult[]\n}\n\nexport function isEntryProposal(proposal: any): boolean {\n return proposal?.kind === 'entry' && Array.isArray(proposal?.rows)\n}\n\nexport function entryReadyCount(proposal: any): number {\n return (proposal?.rows ?? []).filter((row: EntryRowView) => !row.issues?.some(issue => issue.blocking)).length\n}\n\n/** The i18n key of an issue - the target's own, or the framework's by code. */\nexport function entryIssueKey(issue: EntryIssueView): string {\n return issue.messageKey || `ai-assistant.entry.issue.${issue.code}`\n}\n\n/** The issues of one cell, and of the row as a whole (no field). */\nexport function entryCellIssues(row: EntryRowView, field?: string): EntryIssueView[] {\n return (row.issues ?? []).filter(issue => (field ? issue.field === field : !issue.field))\n}\n\n/** What stays in the conversation - the model reads it on its next turn, and the person sees what went in. */\nexport function entryOutcomeNote(answer: EntryApplyAnswer, t: (key: string, options?: any) => string): string {\n const refused = (answer.rows ?? []).filter(row => !row.ok)\n const head = answer.applied\n ? t('ai-assistant.entry.entered', { defaultValue: 'entered {count} rows.', count: answer.applied })\n : t('ai-assistant.entry.entered-none', { defaultValue: 'no row was entered.' })\n const lines = refused.slice(0, 20).map(row => t('ai-assistant.entry.row-refused', { defaultValue: 'row {n}: {reason}', n: row.index + 1, reason: row.error || '' }))\n const more = refused.length > 20 ? [t('ai-assistant.entry.more-refused', { defaultValue: '{count} more rows were not entered.', count: refused.length - 20 })] : []\n return [head, ...lines, ...more].join('\\n')\n}\n\n/** The handler every entry card runs - built with the mutation so the rules are tested without a server. */\nexport function entryProposalHandler(\n apply: (sessionId: string, proposalKey: string) => Promise<EntryApplyAnswer>,\n t: (key: string, options?: any) => string\n) {\n return async (proposal: any, context: ProposalContext): Promise<ProposalOutcome> => {\n if (!context.sessionId || !proposal?.key) return { verdict: 'unavailable', errorCode: 'no-conversation' }\n const answer = await apply(context.sessionId, proposal.key)\n return {\n verdict: answer.verdict,\n ...(answer.errorCode ? { errorCode: answer.errorCode } : {}),\n applied: answer.applied ?? 0,\n missed: answer.missed ?? 0,\n note: entryOutcomeNote(answer, t)\n }\n }\n}\n\n/**\n * The newest card per record, over the conversation's proposals in order - an older one is replaced and\n * cannot be entered (the server refuses it as `superseded`; the card says so before anyone presses).\n */\nexport function latestEntryKeys(proposalsInOrder: any[][]): Map<string, string> {\n const latest = new Map<string, string>()\n for (const list of proposalsInOrder) for (const one of list ?? []) if (isEntryProposal(one) && one.key) latest.set(one.target, one.key)\n return latest\n}\n"]}
@@ -0,0 +1,40 @@
1
+ export interface WorkspaceBlockView {
2
+ kind: string;
3
+ [key: string]: any;
4
+ }
5
+ export interface WorkspaceView {
6
+ name: string;
7
+ title: string;
8
+ summary?: string;
9
+ blocks: WorkspaceBlockView[];
10
+ }
11
+ export interface WorkspaceVersion {
12
+ /** 1-based, in the order the conversation made them. */
13
+ version: number;
14
+ messageId: string;
15
+ createdAt?: string;
16
+ workspace: WorkspaceView;
17
+ }
18
+ export declare function isWorkspaceProposal(proposal: any): boolean;
19
+ /**
20
+ * Every version of the named workspace in a conversation, oldest first. A message the reader may not see
21
+ * comes back redacted without its proposals, so a version behind a door is simply not among them.
22
+ */
23
+ export declare function workspaceVersions(messages: {
24
+ id: string;
25
+ createdAt?: string;
26
+ proposalsJson?: any[] | null;
27
+ }[], name: string): WorkspaceVersion[];
28
+ /** The version of a card within its conversation - "2 of 3" on the card and on the page. */
29
+ export declare function versionOfCard(proposalsInOrder: any[][], name: string, key: string): {
30
+ version: number;
31
+ of: number;
32
+ };
33
+ export declare function workspaceAddress(sessionId: string, name: string, version?: number): string;
34
+ /** Sorts by one column - numbers as numbers, empty last, the rest by the reader's collation. */
35
+ export declare function sortRows(rows: Record<string, any>[], column: string, direction: 'asc' | 'desc', locale?: string): Record<string, any>[];
36
+ /** The table as CSV - the heading row in the column labels, a BOM so a spreadsheet reads Korean. */
37
+ export declare function tableCsv(columns: {
38
+ name: string;
39
+ label?: string;
40
+ }[], rows: Record<string, any>[]): string;
@@ -0,0 +1,68 @@
1
+ /*
2
+ * The AI 작업판 on the screen - which versions a workspace has, and the table's sort and CSV
3
+ * (operato-application/design/ai/workspace.md). Pure - tested without a browser.
4
+ */
5
+ export function isWorkspaceProposal(proposal) {
6
+ return proposal?.kind === 'workspace' && !!proposal?.workspace?.name;
7
+ }
8
+ /**
9
+ * Every version of the named workspace in a conversation, oldest first. A message the reader may not see
10
+ * comes back redacted without its proposals, so a version behind a door is simply not among them.
11
+ */
12
+ export function workspaceVersions(messages, name) {
13
+ const versions = [];
14
+ for (const message of messages) {
15
+ for (const proposal of message.proposalsJson ?? []) {
16
+ if (isWorkspaceProposal(proposal) && proposal.workspace.name === name) {
17
+ versions.push({ version: versions.length + 1, messageId: message.id, createdAt: message.createdAt, workspace: proposal.workspace });
18
+ }
19
+ }
20
+ }
21
+ return versions;
22
+ }
23
+ /** The version of a card within its conversation - "2 of 3" on the card and on the page. */
24
+ export function versionOfCard(proposalsInOrder, name, key) {
25
+ let version = 0;
26
+ let of = 0;
27
+ for (const list of proposalsInOrder)
28
+ for (const one of list ?? []) {
29
+ if (!isWorkspaceProposal(one) || one.workspace.name !== name)
30
+ continue;
31
+ of += 1;
32
+ if (one.key === key)
33
+ version = of;
34
+ }
35
+ return { version, of };
36
+ }
37
+ export function workspaceAddress(sessionId, name, version) {
38
+ const query = new URLSearchParams({ session: sessionId, name });
39
+ if (version)
40
+ query.set('version', String(version));
41
+ return `assistant-workspace?${query.toString()}`;
42
+ }
43
+ /** Sorts by one column - numbers as numbers, empty last, the rest by the reader's collation. */
44
+ export function sortRows(rows, column, direction, locale) {
45
+ const factor = direction === 'asc' ? 1 : -1;
46
+ const collator = new Intl.Collator(locale, { numeric: true });
47
+ return [...rows].sort((a, b) => {
48
+ const x = a[column];
49
+ const y = b[column];
50
+ const emptyX = x === null || x === undefined || x === '';
51
+ const emptyY = y === null || y === undefined || y === '';
52
+ if (emptyX || emptyY)
53
+ return emptyX === emptyY ? 0 : emptyX ? 1 : -1;
54
+ if (typeof x === 'number' && typeof y === 'number')
55
+ return (x - y) * factor;
56
+ return collator.compare(String(x), String(y)) * factor;
57
+ });
58
+ }
59
+ const csvCell = (value) => {
60
+ const text = value === null || value === undefined ? '' : String(value);
61
+ return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
62
+ };
63
+ /** The table as CSV - the heading row in the column labels, a BOM so a spreadsheet reads Korean. */
64
+ export function tableCsv(columns, rows) {
65
+ const lines = [columns.map(column => csvCell(column.label || column.name)).join(','), ...rows.map(row => columns.map(column => csvCell(row[column.name])).join(','))];
66
+ return '' + lines.join('\r\n');
67
+ }
68
+ //# sourceMappingURL=workspace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../client/utils/workspace.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The AI 작업판 on the screen - which versions a workspace has, and the table's sort and CSV\n * (operato-application/design/ai/workspace.md). Pure - tested without a browser.\n */\n\nexport interface WorkspaceBlockView {\n kind: string\n [key: string]: any\n}\n\nexport interface WorkspaceView {\n name: string\n title: string\n summary?: string\n blocks: WorkspaceBlockView[]\n}\n\nexport interface WorkspaceVersion {\n /** 1-based, in the order the conversation made them. */\n version: number\n messageId: string\n createdAt?: string\n workspace: WorkspaceView\n}\n\nexport function isWorkspaceProposal(proposal: any): boolean {\n return proposal?.kind === 'workspace' && !!proposal?.workspace?.name\n}\n\n/**\n * Every version of the named workspace in a conversation, oldest first. A message the reader may not see\n * comes back redacted without its proposals, so a version behind a door is simply not among them.\n */\nexport function workspaceVersions(messages: { id: string; createdAt?: string; proposalsJson?: any[] | null }[], name: string): WorkspaceVersion[] {\n const versions: WorkspaceVersion[] = []\n for (const message of messages) {\n for (const proposal of message.proposalsJson ?? []) {\n if (isWorkspaceProposal(proposal) && proposal.workspace.name === name) {\n versions.push({ version: versions.length + 1, messageId: message.id, createdAt: message.createdAt, workspace: proposal.workspace })\n }\n }\n }\n return versions\n}\n\n/** The version of a card within its conversation - \"2 of 3\" on the card and on the page. */\nexport function versionOfCard(proposalsInOrder: any[][], name: string, key: string): { version: number; of: number } {\n let version = 0\n let of = 0\n for (const list of proposalsInOrder)\n for (const one of list ?? []) {\n if (!isWorkspaceProposal(one) || one.workspace.name !== name) continue\n of += 1\n if (one.key === key) version = of\n }\n return { version, of }\n}\n\nexport function workspaceAddress(sessionId: string, name: string, version?: number): string {\n const query = new URLSearchParams({ session: sessionId, name })\n if (version) query.set('version', String(version))\n return `assistant-workspace?${query.toString()}`\n}\n\n/** Sorts by one column - numbers as numbers, empty last, the rest by the reader's collation. */\nexport function sortRows(rows: Record<string, any>[], column: string, direction: 'asc' | 'desc', locale?: string): Record<string, any>[] {\n const factor = direction === 'asc' ? 1 : -1\n const collator = new Intl.Collator(locale, { numeric: true })\n return [...rows].sort((a, b) => {\n const x = a[column]\n const y = b[column]\n const emptyX = x === null || x === undefined || x === ''\n const emptyY = y === null || y === undefined || y === ''\n if (emptyX || emptyY) return emptyX === emptyY ? 0 : emptyX ? 1 : -1\n if (typeof x === 'number' && typeof y === 'number') return (x - y) * factor\n return collator.compare(String(x), String(y)) * factor\n })\n}\n\nconst csvCell = (value: unknown) => {\n const text = value === null || value === undefined ? '' : String(value)\n return /[\",\\n\\r]/.test(text) ? `\"${text.replace(/\"/g, '\"\"')}\"` : text\n}\n\n/** The table as CSV - the heading row in the column labels, a BOM so a spreadsheet reads Korean. */\nexport function tableCsv(columns: { name: string; label?: string }[], rows: Record<string, any>[]): string {\n const lines = [columns.map(column => csvCell(column.label || column.name)).join(','), ...rows.map(row => columns.map(column => csvCell(row[column.name])).join(','))]\n return '' + lines.join('\\r\\n')\n}\n"]}
@@ -19,6 +19,7 @@ exports.clearAssistantModes = clearAssistantModes;
19
19
  * registration: `getAssistantModeWarnings()` is the answer as of now, and the boot report reads it
20
20
  * after every module is in.
21
21
  */
22
+ const entry_target_js_1 = require("../data-entry/entry-target.js");
22
23
  const ai_client_base_1 = require("@things-factory/ai-client-base");
23
24
  const modes = new Map();
24
25
  const KIND = /^[a-z][a-z0-9-]*\.[a-z][a-z0-9-]*$/;
@@ -94,6 +95,18 @@ function getAssistantModeWarnings() {
94
95
  });
95
96
  }
96
97
  }
98
+ for (const mode of modes.values()) {
99
+ /* A record the mode says it enters, which no app registered - the tool never appears and the person is told nothing. */
100
+ for (const key of mode.entryTargets ?? []) {
101
+ if ((0, entry_target_js_1.entryTargetOf)(key))
102
+ continue;
103
+ out.push({
104
+ code: 'assistant-mode-entry-target-unknown',
105
+ params: { kind: mode.kind, target: key },
106
+ message: `mode '${mode.kind}' enters '${key}', which no application registered with registerEntryTarget`
107
+ });
108
+ }
109
+ }
97
110
  for (const mode of modes.values()) {
98
111
  const exposed = new Set((0, ai_client_base_1.getToolSpecsFor)(mode.toolCategories ?? []).map(spec => spec.name));
99
112
  for (const slash of mode.slashTemplates ?? []) {
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/registry.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The mode registry — one place that knows what each conversation is.\n *\n * Registration refuses what cannot be true later: a kind that is not `<app>.<surface>` (the door\n * looks a mode up by that name), an empty persona (the field this whole change exists to take off\n * the wire), and a second declaration of a kind that is already declared — two declarations of one\n * conversation disagree, and the one that wins would be whichever module booted last.\n *\n * What it does not refuse is a `/` sentence naming a tool the mode does not expose. That check\n * reads the tool registry, which fills up as modules boot, so it runs when asked rather than at\n * registration: `getAssistantModeWarnings()` is the answer as of now, and the boot report reads it\n * after every module is in.\n */\nimport { getToolSpecsFor } from '@things-factory/ai-client-base'\n\nimport type { AssistantModeSpec, AssistantModeWarning } from './types'\n\nconst modes = new Map<string, AssistantModeSpec>()\n\nconst KIND = /^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$/\n\nexport function registerAssistantMode(spec: AssistantModeSpec): void {\n if (!KIND.test(String(spec?.kind ?? ''))) {\n throw new Error(\n `[ai-assistant/assistant-mode] '${spec?.kind}' is not a mode kind. Use <app>.<surface> in lowercase — ` +\n \"'twin.space', 'plant.station', 'figure.modeller'. The client names the conversation by this.\"\n )\n }\n\n if (!String(spec.systemPrompt ?? '').trim()) {\n throw new Error(\n `[ai-assistant/assistant-mode] mode '${spec.kind}' has no systemPrompt. Who the assistant is on a ` +\n 'surface belongs to the server that owns the surface (ADR-0061 decision 7).'\n )\n }\n\n if (modes.has(spec.kind)) {\n throw new Error(\n `[ai-assistant/assistant-mode] mode '${spec.kind}' is already registered. Two declarations of one ` +\n 'conversation disagree, and which one wins would depend on boot order. Give the second one its own kind.'\n )\n }\n\n modes.set(spec.kind, spec)\n}\n\nexport function getAssistantMode(kind: string): AssistantModeSpec | undefined {\n return modes.get(kind)\n}\n\nexport function listAssistantModes(): ReadonlyArray<AssistantModeSpec> {\n return Array.from(modes.values())\n}\n\n/**\n * The promises a mode makes that its tools cannot keep, as of now.\n *\n * Read after boot: a mode may register before the category its slash sentences use.\n */\nexport function getAssistantModeWarnings(): ReadonlyArray<AssistantModeWarning> {\n const out: AssistantModeWarning[] = []\n\n /*\n * 한 화면이 어느 대화를 기본으로 여는가 — 등록 순서로 정하지 않는다(ADR-0072 A17 ②). 없으면 화면이\n * 무엇을 열지 모르고, 둘이면 마지막에 부팅한 모듈이 조용히 이긴다. 둘 다 여기서 말한다.\n */\n const defaults = new Map<string, string[]>()\n const claimed = new Set<string>()\n for (const mode of modes.values()) {\n for (const subject of mode.subjects ?? []) claimed.add(subject)\n for (const subject of mode.defaultFor ?? []) {\n defaults.set(subject, [...(defaults.get(subject) ?? []), mode.kind])\n }\n }\n for (const subject of claimed) {\n const owners = defaults.get(subject) ?? []\n if (owners.length === 0) {\n out.push({\n code: 'assistant-mode-subject-without-default',\n params: { kind: '', subject },\n message: `no conversation is the default for subject '${subject}' — a screen showing it has nothing to open`\n })\n } else if (owners.length > 1) {\n out.push({\n code: 'assistant-mode-subject-default-twice',\n params: { kind: owners[0], subject, kinds: owners },\n message: `subject '${subject}' has more than one default (${owners.join(', ')}) — boot order would decide it`\n })\n }\n }\n\n for (const mode of modes.values()) {\n /*\n * 고를 수 있는 대화에는 부를 이름이 있어야 한다 — 고르개에 종류 이름(`twin.space`)이 뜨면 그것은\n * 우리 내부 단위를 사람에게 떠넘기는 것이다.\n */\n const shared = (mode.subjects ?? []).some(subject => countModesFor(subject) > 1)\n if (shared && !mode.labelKey) {\n out.push({\n code: 'assistant-mode-offered-without-label',\n params: { kind: mode.kind },\n message: `mode '${mode.kind}' can be offered beside another on the same subject but has no labelKey`\n })\n }\n }\n\n for (const mode of modes.values()) {\n const exposed = new Set(getToolSpecsFor(mode.toolCategories ?? []).map(spec => spec.name))\n for (const slash of mode.slashTemplates ?? []) {\n for (const tool of slash.tools ?? []) {\n if (exposed.has(tool)) continue\n out.push({\n code: 'assistant-mode-slash-outside-tools',\n params: { kind: mode.kind, slash: slash.name, tool },\n message:\n `mode '${mode.kind}' offers '/${slash.name}', which needs '${tool}' — not in its tool categories ` +\n `[${(mode.toolCategories ?? []).join(', ')}]. The person presses it and the model answers without doing it.`\n })\n }\n }\n }\n return out\n}\n\n/** 이 subject 종류에 서는 대화가 몇인가 — 고르개가 필요한지, 이름이 필요한지를 가른다. */\nfunction countModesFor(subject: string): number {\n let count = 0\n for (const mode of modes.values()) {\n if ((mode.subjects ?? []).includes(subject)) count++\n }\n return count\n}\n\n/**\n * 이 subject 종류에 서는 대화들 — 등록 순서대로, 기본이 먼저.\n *\n * 화면은 이 목록으로 고르개를 낸다. 하나뿐이면 고르개를 내지 않는다(ADR-0072 A17 ①).\n */\nexport function assistantModesForSubject(subject: string): ReadonlyArray<AssistantModeSpec> {\n const standing = [...modes.values()].filter(mode => (mode.subjects ?? []).includes(subject))\n return standing.sort((a, b) => Number((b.defaultFor ?? []).includes(subject)) - Number((a.defaultFor ?? []).includes(subject)))\n}\n\n/** Test only. */\nexport function clearAssistantModes(): void {\n modes.clear()\n}\n"]}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/registry.ts"],"names":[],"mappings":";;AAsBA,sDAuBC;AAED,4CAEC;AAED,gDAEC;AAOD,4DA2EC;AAgBD,4DAGC;AAGD,kDAEC;AA/JD;;;;;;;;;;;;GAYG;AACH,mEAA6D;AAC7D,mEAAgE;AAIhE,MAAM,KAAK,GAAG,IAAI,GAAG,EAA6B,CAAA;AAElD,MAAM,IAAI,GAAG,oCAAoC,CAAA;AAEjD,SAAgB,qBAAqB,CAAC,IAAuB;IAC3D,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;QACzC,MAAM,IAAI,KAAK,CACb,kCAAkC,IAAI,EAAE,IAAI,2DAA2D;YACrG,8FAA8F,CACjG,CAAA;IACH,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAC5C,MAAM,IAAI,KAAK,CACb,uCAAuC,IAAI,CAAC,IAAI,mDAAmD;YACjG,4EAA4E,CAC/E,CAAA;IACH,CAAC;IAED,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CACb,uCAAuC,IAAI,CAAC,IAAI,mDAAmD;YACjG,yGAAyG,CAC5G,CAAA;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;AAC5B,CAAC;AAED,SAAgB,gBAAgB,CAAC,IAAY;IAC3C,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AACxB,CAAC;AAED,SAAgB,kBAAkB;IAChC,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAA;AACnC,CAAC;AAED;;;;GAIG;AACH,SAAgB,wBAAwB;IACtC,MAAM,GAAG,GAA2B,EAAE,CAAA;IAEtC;;;OAGG;IACH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAoB,CAAA;IAC5C,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAA;IACjC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;QAClC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,QAAQ,IAAI,EAAE;YAAE,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAC/D,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,UAAU,IAAI,EAAE,EAAE,CAAC;YAC5C,QAAQ,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QACtE,CAAC;IACH,CAAC;IACD,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAA;QAC1C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACxB,GAAG,CAAC,IAAI,CAAC;gBACP,IAAI,EAAE,wCAAwC;gBAC9C,MAAM,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE;gBAC7B,OAAO,EAAE,+CAA+C,OAAO,6CAA6C;aAC7G,CAAC,CAAA;QACJ,CAAC;aAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,GAAG,CAAC,IAAI,CAAC;gBACP,IAAI,EAAE,sCAAsC;gBAC5C,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE;gBACnD,OAAO,EAAE,YAAY,OAAO,gCAAgC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,gCAAgC;aAC9G,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;QAClC;;;WAGG;QACH,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAA;QAChF,IAAI,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC7B,GAAG,CAAC,IAAI,CAAC;gBACP,IAAI,EAAE,sCAAsC;gBAC5C,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;gBAC3B,OAAO,EAAE,SAAS,IAAI,CAAC,IAAI,yEAAyE;aACrG,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;QAClC,wHAAwH;QACxH,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,YAAY,IAAI,EAAE,EAAE,CAAC;YAC1C,IAAI,IAAA,+BAAa,EAAC,GAAG,CAAC;gBAAE,SAAQ;YAChC,GAAG,CAAC,IAAI,CAAC;gBACP,IAAI,EAAE,qCAAqC;gBAC3C,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;gBACxC,OAAO,EAAE,SAAS,IAAI,CAAC,IAAI,aAAa,GAAG,6DAA6D;aACzG,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;QAClC,MAAM,OAAO,GAAG,IAAI,GAAG,CAAC,IAAA,gCAAe,EAAC,IAAI,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QAC1F,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,cAAc,IAAI,EAAE,EAAE,CAAC;YAC9C,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;gBACrC,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;oBAAE,SAAQ;gBAC/B,GAAG,CAAC,IAAI,CAAC;oBACP,IAAI,EAAE,oCAAoC;oBAC1C,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE;oBACpD,OAAO,EACL,SAAS,IAAI,CAAC,IAAI,cAAc,KAAK,CAAC,IAAI,mBAAmB,IAAI,iCAAiC;wBAClG,IAAI,CAAC,IAAI,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,kEAAkE;iBAC/G,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,2DAA2D;AAC3D,SAAS,aAAa,CAAC,OAAe;IACpC,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC;YAAE,KAAK,EAAE,CAAA;IACtD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;GAIG;AACH,SAAgB,wBAAwB,CAAC,OAAe;IACtD,MAAM,QAAQ,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;IAC5F,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AACjI,CAAC;AAED,iBAAiB;AACjB,SAAgB,mBAAmB;IACjC,KAAK,CAAC,KAAK,EAAE,CAAA;AACf,CAAC","sourcesContent":["/**\n * The mode registry — one place that knows what each conversation is.\n *\n * Registration refuses what cannot be true later: a kind that is not `<app>.<surface>` (the door\n * looks a mode up by that name), an empty persona (the field this whole change exists to take off\n * the wire), and a second declaration of a kind that is already declared — two declarations of one\n * conversation disagree, and the one that wins would be whichever module booted last.\n *\n * What it does not refuse is a `/` sentence naming a tool the mode does not expose. That check\n * reads the tool registry, which fills up as modules boot, so it runs when asked rather than at\n * registration: `getAssistantModeWarnings()` is the answer as of now, and the boot report reads it\n * after every module is in.\n */\nimport { entryTargetOf } from '../data-entry/entry-target.js'\nimport { getToolSpecsFor } from '@things-factory/ai-client-base'\n\nimport type { AssistantModeSpec, AssistantModeWarning } from './types'\n\nconst modes = new Map<string, AssistantModeSpec>()\n\nconst KIND = /^[a-z][a-z0-9-]*\\.[a-z][a-z0-9-]*$/\n\nexport function registerAssistantMode(spec: AssistantModeSpec): void {\n if (!KIND.test(String(spec?.kind ?? ''))) {\n throw new Error(\n `[ai-assistant/assistant-mode] '${spec?.kind}' is not a mode kind. Use <app>.<surface> in lowercase — ` +\n \"'twin.space', 'plant.station', 'figure.modeller'. The client names the conversation by this.\"\n )\n }\n\n if (!String(spec.systemPrompt ?? '').trim()) {\n throw new Error(\n `[ai-assistant/assistant-mode] mode '${spec.kind}' has no systemPrompt. Who the assistant is on a ` +\n 'surface belongs to the server that owns the surface (ADR-0061 decision 7).'\n )\n }\n\n if (modes.has(spec.kind)) {\n throw new Error(\n `[ai-assistant/assistant-mode] mode '${spec.kind}' is already registered. Two declarations of one ` +\n 'conversation disagree, and which one wins would depend on boot order. Give the second one its own kind.'\n )\n }\n\n modes.set(spec.kind, spec)\n}\n\nexport function getAssistantMode(kind: string): AssistantModeSpec | undefined {\n return modes.get(kind)\n}\n\nexport function listAssistantModes(): ReadonlyArray<AssistantModeSpec> {\n return Array.from(modes.values())\n}\n\n/**\n * The promises a mode makes that its tools cannot keep, as of now.\n *\n * Read after boot: a mode may register before the category its slash sentences use.\n */\nexport function getAssistantModeWarnings(): ReadonlyArray<AssistantModeWarning> {\n const out: AssistantModeWarning[] = []\n\n /*\n * 한 화면이 어느 대화를 기본으로 여는가 — 등록 순서로 정하지 않는다(ADR-0072 A17 ②). 없으면 화면이\n * 무엇을 열지 모르고, 둘이면 마지막에 부팅한 모듈이 조용히 이긴다. 둘 다 여기서 말한다.\n */\n const defaults = new Map<string, string[]>()\n const claimed = new Set<string>()\n for (const mode of modes.values()) {\n for (const subject of mode.subjects ?? []) claimed.add(subject)\n for (const subject of mode.defaultFor ?? []) {\n defaults.set(subject, [...(defaults.get(subject) ?? []), mode.kind])\n }\n }\n for (const subject of claimed) {\n const owners = defaults.get(subject) ?? []\n if (owners.length === 0) {\n out.push({\n code: 'assistant-mode-subject-without-default',\n params: { kind: '', subject },\n message: `no conversation is the default for subject '${subject}' — a screen showing it has nothing to open`\n })\n } else if (owners.length > 1) {\n out.push({\n code: 'assistant-mode-subject-default-twice',\n params: { kind: owners[0], subject, kinds: owners },\n message: `subject '${subject}' has more than one default (${owners.join(', ')}) — boot order would decide it`\n })\n }\n }\n\n for (const mode of modes.values()) {\n /*\n * 고를 수 있는 대화에는 부를 이름이 있어야 한다 — 고르개에 종류 이름(`twin.space`)이 뜨면 그것은\n * 우리 내부 단위를 사람에게 떠넘기는 것이다.\n */\n const shared = (mode.subjects ?? []).some(subject => countModesFor(subject) > 1)\n if (shared && !mode.labelKey) {\n out.push({\n code: 'assistant-mode-offered-without-label',\n params: { kind: mode.kind },\n message: `mode '${mode.kind}' can be offered beside another on the same subject but has no labelKey`\n })\n }\n }\n\n for (const mode of modes.values()) {\n /* A record the mode says it enters, which no app registered - the tool never appears and the person is told nothing. */\n for (const key of mode.entryTargets ?? []) {\n if (entryTargetOf(key)) continue\n out.push({\n code: 'assistant-mode-entry-target-unknown',\n params: { kind: mode.kind, target: key },\n message: `mode '${mode.kind}' enters '${key}', which no application registered with registerEntryTarget`\n })\n }\n }\n\n for (const mode of modes.values()) {\n const exposed = new Set(getToolSpecsFor(mode.toolCategories ?? []).map(spec => spec.name))\n for (const slash of mode.slashTemplates ?? []) {\n for (const tool of slash.tools ?? []) {\n if (exposed.has(tool)) continue\n out.push({\n code: 'assistant-mode-slash-outside-tools',\n params: { kind: mode.kind, slash: slash.name, tool },\n message:\n `mode '${mode.kind}' offers '/${slash.name}', which needs '${tool}' — not in its tool categories ` +\n `[${(mode.toolCategories ?? []).join(', ')}]. The person presses it and the model answers without doing it.`\n })\n }\n }\n }\n return out\n}\n\n/** 이 subject 종류에 서는 대화가 몇인가 — 고르개가 필요한지, 이름이 필요한지를 가른다. */\nfunction countModesFor(subject: string): number {\n let count = 0\n for (const mode of modes.values()) {\n if ((mode.subjects ?? []).includes(subject)) count++\n }\n return count\n}\n\n/**\n * 이 subject 종류에 서는 대화들 — 등록 순서대로, 기본이 먼저.\n *\n * 화면은 이 목록으로 고르개를 낸다. 하나뿐이면 고르개를 내지 않는다(ADR-0072 A17 ①).\n */\nexport function assistantModesForSubject(subject: string): ReadonlyArray<AssistantModeSpec> {\n const standing = [...modes.values()].filter(mode => (mode.subjects ?? []).includes(subject))\n return standing.sort((a, b) => Number((b.defaultFor ?? []).includes(subject)) - Number((a.defaultFor ?? []).includes(subject)))\n}\n\n/** Test only. */\nexport function clearAssistantModes(): void {\n modes.clear()\n}\n"]}
@@ -14,6 +14,17 @@ export interface ModeSettings {
14
14
  temperature?: number;
15
15
  maxTokens?: number;
16
16
  }
17
+ /**
18
+ * The categories a mode opens: its own, the AI 작업판 (design/ai/workspace.md), the attachment reader when it
19
+ * takes files (design/ai/shared-dock.md §9-4), and one propose tool per record it may enter (design/ai/data-entry.md).
20
+ */
21
+ export declare function modeToolCategories(mode: {
22
+ toolCategories?: string[];
23
+ attachments?: {
24
+ kinds?: string[];
25
+ };
26
+ entryTargets?: string[];
27
+ }): string[] | undefined;
17
28
  /** The conversation's settings, from the server's declaration. */
18
29
  export declare function resolveModeSettings(ref: AssistantModeRef, t: Translate): ModeSettings;
19
30
  export interface AssistantModeSlashView {
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.modeToolCategories = modeToolCategories;
3
4
  exports.resolveModeSettings = resolveModeSettings;
4
5
  exports.assistantModeView = assistantModeView;
5
6
  /**
@@ -23,6 +24,19 @@ function unknownModeError(kind, t) {
23
24
  error.params = { kind };
24
25
  return error;
25
26
  }
27
+ /**
28
+ * The categories a mode opens: its own, the AI 작업판 (design/ai/workspace.md), the attachment reader when it
29
+ * takes files (design/ai/shared-dock.md §9-4), and one propose tool per record it may enter (design/ai/data-entry.md).
30
+ */
31
+ function modeToolCategories(mode) {
32
+ const extra = [
33
+ /* Every conversation may put a large result on the AI 작업판 (design/ai/workspace.md). */
34
+ 'assistant-workspace',
35
+ ...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),
36
+ ...(mode.entryTargets ?? []).map(key => `data-entry:${key}`)
37
+ ];
38
+ return [...new Set([...(mode.toolCategories ?? []), ...extra])];
39
+ }
26
40
  /** The conversation's settings, from the server's declaration. */
27
41
  function resolveModeSettings(ref, t) {
28
42
  const kind = String(ref?.kind ?? '');
@@ -32,9 +46,7 @@ function resolveModeSettings(ref, t) {
32
46
  return {
33
47
  systemPrompt: mode.systemPrompt,
34
48
  /* A mode that takes attachments can open the files sent in earlier turns (design/ai/shared-dock.md §9-4). */
35
- toolCategories: mode.attachments?.kinds?.length
36
- ? [...(mode.toolCategories ?? []), 'chat-attachments']
37
- : mode.toolCategories,
49
+ toolCategories: modeToolCategories(mode),
38
50
  requireToolOnFirstTurn: mode.requireToolOnFirstTurn,
39
51
  model: mode.model,
40
52
  temperature: mode.temperature,
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AttachmentPolicy } from '../chat-session/attachment-policy.js'\n/**\n * What the door and the screen each take from a mode.\n *\n * Both start from the same declaration, so a screen cannot show `/` sentences for a conversation\n * the server would answer differently.\n */\nimport { getAssistantMode } from './registry'\nimport type { AssistantModeSlash } from './types'\n\nexport type Translate = (key: string, params?: Record<string, any>) => string\n\nexport interface AssistantModeRef {\n /** Which conversation this is — a registered `<app>.<surface>`. */\n kind: string\n /** What it is about. Part of the conversation's lifetime; the tools read it, not the settings. */\n target?: string\n}\n\n/**\n * A kind nobody registered. The sentence is the person's (ADR-0054); the code and the kind are for\n * whoever reads the log — a screen naming a conversation the server does not have is a mismatched\n * deployment, not something the person did.\n */\nfunction unknownModeError(kind: string, t: Translate): Error {\n const error: any = new Error(\n t('ai-assistant.text.unknown-mode', {\n defaultValue: 'this screen asked for a conversation this server does not have ({kind}). reload the page.',\n kind: kind || '—'\n })\n )\n error.code = 'unknown-assistant-mode'\n error.params = { kind }\n return error\n}\n\nexport interface ModeSettings {\n systemPrompt: string\n toolCategories?: string[]\n requireToolOnFirstTurn?: boolean\n model?: string\n temperature?: number\n maxTokens?: number\n}\n\n/** The conversation's settings, from the server's declaration. */\nexport function resolveModeSettings(ref: AssistantModeRef, t: Translate): ModeSettings {\n const kind = String(ref?.kind ?? '')\n const mode = kind ? getAssistantMode(kind) : undefined\n\n if (!mode) throw unknownModeError(kind, t)\n\n return {\n systemPrompt: mode.systemPrompt,\n /* A mode that takes attachments can open the files sent in earlier turns (design/ai/shared-dock.md §9-4). */\n toolCategories: mode.attachments?.kinds?.length\n ? [...(mode.toolCategories ?? []), 'chat-attachments']\n : mode.toolCategories,\n requireToolOnFirstTurn: mode.requireToolOnFirstTurn,\n model: mode.model,\n temperature: mode.temperature,\n maxTokens: mode.maxTokens\n }\n}\n\nexport interface AssistantModeSlashView {\n name: string\n description: string\n template: string\n}\n\nexport interface AssistantModeView {\n kind: string\n slashTemplates: AssistantModeSlashView[]\n catalogEntries: any[]\n proposalChoices: any[]\n placeholder?: string\n /** 빈 대화의 제목과 한 줄 — 이 대화가 무엇을 하는 자리인지. */\n intro?: { title: string; subtitle: string }\n /** 빈 대화의 예시 — 이 대화에서 실제로 할 수 있는 말. */\n examples?: Array<{ label: string; items: string[] }>\n footerNotice?: string\n stagedNotice?: string\n /** What the screen may attach - the same policy the server enforces. */\n attachments?: AttachmentPolicy\n}\n\n/** What the screen shows, in the words of the request's locale. */\nexport function assistantModeView(kind: string, t: Translate): AssistantModeView {\n const mode = getAssistantMode(kind)\n if (!mode) throw unknownModeError(kind, t)\n\n return {\n kind: mode.kind,\n slashTemplates: (mode.slashTemplates ?? []).map((slash: AssistantModeSlash) => ({\n name: slash.name,\n description: t(slash.descriptionKey),\n template: t(slash.templateKey)\n })),\n catalogEntries: mode.catalogEntries ?? [],\n proposalChoices: mode.proposalChoices ?? [],\n placeholder: mode.placeholderKey ? t(mode.placeholderKey) : undefined,\n intro: mode.introKeys ? { title: t(mode.introKeys.title), subtitle: t(mode.introKeys.subtitle) } : undefined,\n examples: mode.exampleKeys?.map(group => ({\n label: t(group.labelKey),\n items: group.itemKeys.map(key => t(key))\n })),\n footerNotice: mode.footerNoticeKey ? t(mode.footerNoticeKey) : undefined,\n stagedNotice: mode.stagedNoticeKey ? t(mode.stagedNoticeKey) : undefined,\n attachments: mode.attachments\n }\n}\n"]}
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AttachmentPolicy } from '../chat-session/attachment-policy.js'\n/**\n * What the door and the screen each take from a mode.\n *\n * Both start from the same declaration, so a screen cannot show `/` sentences for a conversation\n * the server would answer differently.\n */\nimport { getAssistantMode } from './registry'\nimport type { AssistantModeSlash } from './types'\n\nexport type Translate = (key: string, params?: Record<string, any>) => string\n\nexport interface AssistantModeRef {\n /** Which conversation this is — a registered `<app>.<surface>`. */\n kind: string\n /** What it is about. Part of the conversation's lifetime; the tools read it, not the settings. */\n target?: string\n}\n\n/**\n * A kind nobody registered. The sentence is the person's (ADR-0054); the code and the kind are for\n * whoever reads the log — a screen naming a conversation the server does not have is a mismatched\n * deployment, not something the person did.\n */\nfunction unknownModeError(kind: string, t: Translate): Error {\n const error: any = new Error(\n t('ai-assistant.text.unknown-mode', {\n defaultValue: 'this screen asked for a conversation this server does not have ({kind}). reload the page.',\n kind: kind || '—'\n })\n )\n error.code = 'unknown-assistant-mode'\n error.params = { kind }\n return error\n}\n\nexport interface ModeSettings {\n systemPrompt: string\n toolCategories?: string[]\n requireToolOnFirstTurn?: boolean\n model?: string\n temperature?: number\n maxTokens?: number\n}\n\n/**\n * The categories a mode opens: its own, the AI 작업판 (design/ai/workspace.md), the attachment reader when it\n * takes files (design/ai/shared-dock.md §9-4), and one propose tool per record it may enter (design/ai/data-entry.md).\n */\nexport function modeToolCategories(mode: { toolCategories?: string[]; attachments?: { kinds?: string[] }; entryTargets?: string[] }): string[] | undefined {\n const extra = [\n /* Every conversation may put a large result on the AI 작업판 (design/ai/workspace.md). */\n 'assistant-workspace',\n ...(mode.attachments?.kinds?.length ? ['chat-attachments'] : []),\n ...(mode.entryTargets ?? []).map(key => `data-entry:${key}`)\n ]\n return [...new Set([...(mode.toolCategories ?? []), ...extra])]\n}\n\n/** The conversation's settings, from the server's declaration. */\nexport function resolveModeSettings(ref: AssistantModeRef, t: Translate): ModeSettings {\n const kind = String(ref?.kind ?? '')\n const mode = kind ? getAssistantMode(kind) : undefined\n\n if (!mode) throw unknownModeError(kind, t)\n\n return {\n systemPrompt: mode.systemPrompt,\n /* A mode that takes attachments can open the files sent in earlier turns (design/ai/shared-dock.md §9-4). */\n toolCategories: modeToolCategories(mode),\n requireToolOnFirstTurn: mode.requireToolOnFirstTurn,\n model: mode.model,\n temperature: mode.temperature,\n maxTokens: mode.maxTokens\n }\n}\n\nexport interface AssistantModeSlashView {\n name: string\n description: string\n template: string\n}\n\nexport interface AssistantModeView {\n kind: string\n slashTemplates: AssistantModeSlashView[]\n catalogEntries: any[]\n proposalChoices: any[]\n placeholder?: string\n /** 빈 대화의 제목과 한 줄 — 이 대화가 무엇을 하는 자리인지. */\n intro?: { title: string; subtitle: string }\n /** 빈 대화의 예시 — 이 대화에서 실제로 할 수 있는 말. */\n examples?: Array<{ label: string; items: string[] }>\n footerNotice?: string\n stagedNotice?: string\n /** What the screen may attach - the same policy the server enforces. */\n attachments?: AttachmentPolicy\n}\n\n/** What the screen shows, in the words of the request's locale. */\nexport function assistantModeView(kind: string, t: Translate): AssistantModeView {\n const mode = getAssistantMode(kind)\n if (!mode) throw unknownModeError(kind, t)\n\n return {\n kind: mode.kind,\n slashTemplates: (mode.slashTemplates ?? []).map((slash: AssistantModeSlash) => ({\n name: slash.name,\n description: t(slash.descriptionKey),\n template: t(slash.templateKey)\n })),\n catalogEntries: mode.catalogEntries ?? [],\n proposalChoices: mode.proposalChoices ?? [],\n placeholder: mode.placeholderKey ? t(mode.placeholderKey) : undefined,\n intro: mode.introKeys ? { title: t(mode.introKeys.title), subtitle: t(mode.introKeys.subtitle) } : undefined,\n examples: mode.exampleKeys?.map(group => ({\n label: t(group.labelKey),\n items: group.itemKeys.map(key => t(key))\n })),\n footerNotice: mode.footerNoticeKey ? t(mode.footerNoticeKey) : undefined,\n stagedNotice: mode.stagedNoticeKey ? t(mode.stagedNoticeKey) : undefined,\n attachments: mode.attachments\n }\n}\n"]}
@@ -98,6 +98,11 @@ export interface AssistantModeSpec {
98
98
  * screen offers no attach button, so no file is taken that the model would never see.
99
99
  */
100
100
  attachments?: AttachmentPolicy;
101
+ /**
102
+ * Which records this conversation may enter from material (design/ai/data-entry.md) - keys registered
103
+ * with `registerEntryTarget`. Each opens that target's propose tool; its door still decides who gets it.
104
+ */
105
+ entryTargets?: string[];
101
106
  /** i18n key for the line under an empty conversation. */
102
107
  footerNoticeKey?: string;
103
108
  /** i18n key for the line shown once the host reports a proposal staged. */
@@ -113,13 +118,14 @@ export interface AssistantModeWarning {
113
118
  * `assistant-mode-subject-default-twice` — two conversations claim the same subject type.
114
119
  * `assistant-mode-offered-without-label` — a conversation a screen may offer with nothing to call it.
115
120
  */
116
- code: 'assistant-mode-slash-outside-tools' | 'assistant-mode-subject-without-default' | 'assistant-mode-subject-default-twice' | 'assistant-mode-offered-without-label';
121
+ code: 'assistant-mode-slash-outside-tools' | 'assistant-mode-subject-without-default' | 'assistant-mode-subject-default-twice' | 'assistant-mode-offered-without-label' | 'assistant-mode-entry-target-unknown';
117
122
  params: {
118
123
  kind: string;
119
124
  slash?: string;
120
125
  tool?: string;
121
126
  subject?: string;
122
127
  kinds?: string[];
128
+ target?: string;
123
129
  };
124
130
  /** For a person reading the boot log; callers act on the code and params. */
125
131
  message: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { AttachmentPolicy } from '../chat-session/attachment-policy.js'\n/**\n * What a conversation is, declared by the app server that owns the screen (ADR-0061 decision 7).\n *\n * ADR-0058 put these five in one value so they could not fall out of step, but the value lived in\n * the browser and travelled on every request: the persona, the tool categories, the model and the\n * limits were the caller's to choose. A caller choosing its own tool categories is a caller\n * choosing its own authority, so the declaration moved here and the client sends only which one it\n * is talking in (`kind`) and what about (`target`).\n */\n\n/** A `/` sentence this mode offers, and what it needs to be answerable. */\nexport interface AssistantModeSlash {\n /** Short name shown in the list. */\n name: string\n /** i18n key for the one-line description. The server translates it into the request's locale. */\n descriptionKey: string\n /** i18n key for the sentence that lands in the input. */\n templateKey: string\n /**\n * The tools this sentence needs. A mode must not offer work it cannot do (ADR-0058 decision 3),\n * and a name outside `toolCategories` is reported at boot rather than when a person presses it.\n */\n tools?: string[]\n}\n\nexport interface AssistantModeSpec {\n /** `<app>.<surface>` — `twin.space`, `plant.station`, `figure.modeller`. */\n kind: string\n\n /**\n * Which kinds of subject this conversation stands for — `['board', 'space']` (ADR-0072 A17 ①).\n *\n * A screen says what it is showing (`context.subject`), and these say which conversations that\n * subject can carry. It is a list of types and not a predicate on purpose: a list can be counted\n * at boot and pinned by a test, while a function can only be run.\n *\n * On the twin modeller nothing could be asked about operations, because an open board pinned the\n * authoring conversation and that conversation opens one category. A conversation that also\n * stands for `board` is how the same screen carries both.\n */\n subjects?: string[]\n\n /**\n * The conversation a screen opens when the person has not chosen — one per subject type\n * (ADR-0072 A17 ②).\n *\n * Registration order must not decide this: whichever module booted last would silently become\n * the default. Exactly one mode declares each subject type, and the boot report says so when a\n * type has none or more than one.\n */\n defaultFor?: string[]\n\n /**\n * i18n key for what this conversation is called where a person chooses between them —\n * \"이 그림 고치기\", \"이 현장 지금 어때\". A mode a screen may offer needs one.\n */\n labelKey?: string\n /** Who the assistant is here, and what it must not claim to do. The server's, not the caller's. */\n systemPrompt: string\n /** Which tool categories this conversation may use. Absent or empty exposes none. */\n toolCategories?: string[]\n /** Make the model read before it answers — for surfaces whose subject is live state. */\n requireToolOnFirstTurn?: boolean\n /** Model and limits are policy, not a request field. */\n model?: string\n temperature?: number\n maxTokens?: number\n /** What `/` offers. Declared as keys; the server translates. */\n slashTemplates?: AssistantModeSlash[]\n /** What `@` offers. */\n catalogEntries?: any[]\n /**\n * i18n key for the words in the empty input.\n *\n * The screen says what this conversation is for, and that differs by mode: figure's modeller had\n * \"describe the figure you want\" while the person was there to change the figure already open.\n */\n placeholderKey?: string\n\n /**\n * 빈 대화가 무엇을 말하는가 — 제목과 한 줄 (ADR-0061 결정 7).\n *\n * 자리표와 `/` 는 대화를 따라가는데 이 블록만 host 속성에 남아 있었다. 그래서 머리에는 「이 도형\n * 고치기」가 서 있고 본문은 「만들 Figure 를 설명하세요」라고 적혀 있었다(2026-09-18, Figure 레인\n * :3300). 한 대화가 화면에 무엇을 말하는가는 한 자리에서 나와야 한다.\n */\n introKeys?: { title: string; subtitle: string }\n\n /**\n * 빈 대화의 예시 — 「이 대화에서 할 수 있는 말」.\n *\n * 예시는 그 도구로 **실제로 할 수 있는 말**이어야 한다. 할 수 없는 것을 예시로 두면 빈 화면이 거짓을\n * 가르친다. 대화마다 열리는 도구가 다르므로 예시도 대화를 따라간다.\n */\n exampleKeys?: Array<{ labelKey: string; itemKeys: string[] }>\n\n /**\n * What this conversation accepts as attachments (design/ai/shared-dock.md §9-1). Absent: nothing - the\n * screen offers no attach button, so no file is taken that the model would never see.\n */\n attachments?: AttachmentPolicy\n /** i18n key for the line under an empty conversation. */\n footerNoticeKey?: string\n /** i18n key for the line shown once the host reports a proposal staged. */\n stagedNoticeKey?: string\n /** Follow-up chips for a proposal that arrived without its own. */\n proposalChoices?: any[]\n}\n\n/** A declaration that registered but is not what the contract asks for. Code + params, not prose. */\nexport interface AssistantModeWarning {\n /**\n * `assistant-mode-slash-outside-tools` — a `/` sentence needs a tool this mode does not open.\n * `assistant-mode-subject-without-default` — a subject type no conversation claims as its default.\n * `assistant-mode-subject-default-twice` — two conversations claim the same subject type.\n * `assistant-mode-offered-without-label` — a conversation a screen may offer with nothing to call it.\n */\n code:\n | 'assistant-mode-slash-outside-tools'\n | 'assistant-mode-subject-without-default'\n | 'assistant-mode-subject-default-twice'\n | 'assistant-mode-offered-without-label'\n params: { kind: string; slash?: string; tool?: string; subject?: string; kinds?: string[] }\n /** For a person reading the boot log; callers act on the code and params. */\n message: string\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { AttachmentPolicy } from '../chat-session/attachment-policy.js'\n/**\n * What a conversation is, declared by the app server that owns the screen (ADR-0061 decision 7).\n *\n * ADR-0058 put these five in one value so they could not fall out of step, but the value lived in\n * the browser and travelled on every request: the persona, the tool categories, the model and the\n * limits were the caller's to choose. A caller choosing its own tool categories is a caller\n * choosing its own authority, so the declaration moved here and the client sends only which one it\n * is talking in (`kind`) and what about (`target`).\n */\n\n/** A `/` sentence this mode offers, and what it needs to be answerable. */\nexport interface AssistantModeSlash {\n /** Short name shown in the list. */\n name: string\n /** i18n key for the one-line description. The server translates it into the request's locale. */\n descriptionKey: string\n /** i18n key for the sentence that lands in the input. */\n templateKey: string\n /**\n * The tools this sentence needs. A mode must not offer work it cannot do (ADR-0058 decision 3),\n * and a name outside `toolCategories` is reported at boot rather than when a person presses it.\n */\n tools?: string[]\n}\n\nexport interface AssistantModeSpec {\n /** `<app>.<surface>` — `twin.space`, `plant.station`, `figure.modeller`. */\n kind: string\n\n /**\n * Which kinds of subject this conversation stands for — `['board', 'space']` (ADR-0072 A17 ①).\n *\n * A screen says what it is showing (`context.subject`), and these say which conversations that\n * subject can carry. It is a list of types and not a predicate on purpose: a list can be counted\n * at boot and pinned by a test, while a function can only be run.\n *\n * On the twin modeller nothing could be asked about operations, because an open board pinned the\n * authoring conversation and that conversation opens one category. A conversation that also\n * stands for `board` is how the same screen carries both.\n */\n subjects?: string[]\n\n /**\n * The conversation a screen opens when the person has not chosen — one per subject type\n * (ADR-0072 A17 ②).\n *\n * Registration order must not decide this: whichever module booted last would silently become\n * the default. Exactly one mode declares each subject type, and the boot report says so when a\n * type has none or more than one.\n */\n defaultFor?: string[]\n\n /**\n * i18n key for what this conversation is called where a person chooses between them —\n * \"이 그림 고치기\", \"이 현장 지금 어때\". A mode a screen may offer needs one.\n */\n labelKey?: string\n /** Who the assistant is here, and what it must not claim to do. The server's, not the caller's. */\n systemPrompt: string\n /** Which tool categories this conversation may use. Absent or empty exposes none. */\n toolCategories?: string[]\n /** Make the model read before it answers — for surfaces whose subject is live state. */\n requireToolOnFirstTurn?: boolean\n /** Model and limits are policy, not a request field. */\n model?: string\n temperature?: number\n maxTokens?: number\n /** What `/` offers. Declared as keys; the server translates. */\n slashTemplates?: AssistantModeSlash[]\n /** What `@` offers. */\n catalogEntries?: any[]\n /**\n * i18n key for the words in the empty input.\n *\n * The screen says what this conversation is for, and that differs by mode: figure's modeller had\n * \"describe the figure you want\" while the person was there to change the figure already open.\n */\n placeholderKey?: string\n\n /**\n * 빈 대화가 무엇을 말하는가 — 제목과 한 줄 (ADR-0061 결정 7).\n *\n * 자리표와 `/` 는 대화를 따라가는데 이 블록만 host 속성에 남아 있었다. 그래서 머리에는 「이 도형\n * 고치기」가 서 있고 본문은 「만들 Figure 를 설명하세요」라고 적혀 있었다(2026-09-18, Figure 레인\n * :3300). 한 대화가 화면에 무엇을 말하는가는 한 자리에서 나와야 한다.\n */\n introKeys?: { title: string; subtitle: string }\n\n /**\n * 빈 대화의 예시 — 「이 대화에서 할 수 있는 말」.\n *\n * 예시는 그 도구로 **실제로 할 수 있는 말**이어야 한다. 할 수 없는 것을 예시로 두면 빈 화면이 거짓을\n * 가르친다. 대화마다 열리는 도구가 다르므로 예시도 대화를 따라간다.\n */\n exampleKeys?: Array<{ labelKey: string; itemKeys: string[] }>\n\n /**\n * What this conversation accepts as attachments (design/ai/shared-dock.md §9-1). Absent: nothing - the\n * screen offers no attach button, so no file is taken that the model would never see.\n */\n attachments?: AttachmentPolicy\n /**\n * Which records this conversation may enter from material (design/ai/data-entry.md) - keys registered\n * with `registerEntryTarget`. Each opens that target's propose tool; its door still decides who gets it.\n */\n entryTargets?: string[]\n /** i18n key for the line under an empty conversation. */\n footerNoticeKey?: string\n /** i18n key for the line shown once the host reports a proposal staged. */\n stagedNoticeKey?: string\n /** Follow-up chips for a proposal that arrived without its own. */\n proposalChoices?: any[]\n}\n\n/** A declaration that registered but is not what the contract asks for. Code + params, not prose. */\nexport interface AssistantModeWarning {\n /**\n * `assistant-mode-slash-outside-tools` — a `/` sentence needs a tool this mode does not open.\n * `assistant-mode-subject-without-default` — a subject type no conversation claims as its default.\n * `assistant-mode-subject-default-twice` — two conversations claim the same subject type.\n * `assistant-mode-offered-without-label` — a conversation a screen may offer with nothing to call it.\n */\n code:\n | 'assistant-mode-slash-outside-tools'\n | 'assistant-mode-subject-without-default'\n | 'assistant-mode-subject-default-twice'\n | 'assistant-mode-offered-without-label'\n | 'assistant-mode-entry-target-unknown'\n params: { kind: string; slash?: string; tool?: string; subject?: string; kinds?: string[]; target?: string }\n /** For a person reading the boot log; callers act on the code and params. */\n message: string\n}\n"]}
@@ -0,0 +1,46 @@
1
+ import { type EntryTarget } from './entry-target.js';
2
+ export interface StoredProposalMessage {
3
+ id: string;
4
+ proposals: any[];
5
+ }
6
+ export interface EntryRowResult {
7
+ index: number;
8
+ ok: boolean;
9
+ id?: string;
10
+ label?: string;
11
+ /** A blocking issue kept it out, or the target refused it. */
12
+ error?: string;
13
+ }
14
+ export interface EntryApplyOutcome {
15
+ verdict: 'accepted' | 'refused' | 'unavailable';
16
+ errorCode?: string;
17
+ applied: number;
18
+ missed: number;
19
+ rows: EntryRowResult[];
20
+ }
21
+ export interface EntryApplyDeps {
22
+ /** The conversation's messages that carry proposals, oldest first. */
23
+ messages: StoredProposalMessage[];
24
+ /** Whether an outcome line already answers this key. */
25
+ answered: boolean;
26
+ passesDoor(door: string): Promise<boolean>;
27
+ /**
28
+ * Runs the target's mutation for one row with the person's request state - the application's API, its
29
+ * directives and its own checks. Answers what GraphQL answers.
30
+ */
31
+ execute(mutation: string, variables: Record<string, unknown>): Promise<{
32
+ data?: any;
33
+ errors?: readonly {
34
+ message: string;
35
+ }[];
36
+ }>;
37
+ state: any;
38
+ report?(message: string, error: unknown): void;
39
+ }
40
+ export declare function findEntryProposal(messages: StoredProposalMessage[], key: string): {
41
+ proposal?: any;
42
+ superseded: boolean;
43
+ };
44
+ export declare function applyEntryProposal(key: string, deps: EntryApplyDeps, targetOf?: (key: string) => EntryTarget | undefined): Promise<EntryApplyOutcome>;
45
+ /** An outcome line already answers this key - `requested` alone does not: the dock writes it just before calling here. */
46
+ export declare function entryProposalAnswered(key: string, lines: readonly (string | null | undefined)[]): boolean;