@promptbook/cli 0.114.0-11 → 0.114.0-13

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 (145) hide show
  1. package/apps/agents-server/src/app/admin/messages/MessagesClient.tsx +8 -1
  2. package/apps/agents-server/src/app/admin/messages/send-email/SendEmailClient.tsx +193 -63
  3. package/apps/agents-server/src/app/admin/messages/send-email/actions.ts +25 -28
  4. package/apps/agents-server/src/app/admin/messages/send-email/emailTesting.ts +153 -0
  5. package/apps/agents-server/src/app/admin/messages/send-email/emailTestingAccess.ts +30 -0
  6. package/apps/agents-server/src/app/admin/messages/send-email/page.tsx +18 -5
  7. package/apps/agents-server/src/app/agents/[agentName]/AgentChatWrapper.tsx +7 -4
  8. package/apps/agents-server/src/app/agents/[agentName]/chat/AgentGoalChatPlannedMessages.tsx +23 -8
  9. package/apps/agents-server/src/app/agents/[agentName]/chat/CanonicalAgentChatSurface.tsx +14 -3
  10. package/apps/agents-server/src/app/agents/[agentName]/chat/page.tsx +2 -20
  11. package/apps/agents-server/src/app/agents/[agentName]/page.tsx +2 -1
  12. package/apps/agents-server/src/app/agents/[agentName]/projectReferenceActions.ts +61 -0
  13. package/apps/agents-server/src/app/api/internal/agent-goal-chat-planned-messages/route.ts +34 -7
  14. package/apps/agents-server/src/components/AgentProjects/resolveLatestCompletedAgentMessageKey.ts +32 -0
  15. package/apps/agents-server/src/components/AgentProjects/useAgentProjectMarkdownReferences.ts +124 -2
  16. package/apps/agents-server/src/languages/ServerTranslationKeys.ts +3 -0
  17. package/apps/agents-server/src/languages/translations/czech.yaml +3 -0
  18. package/apps/agents-server/src/languages/translations/english.yaml +3 -0
  19. package/apps/agents-server/src/message-providers/email/stalwart/StalwartMessageProvider.ts +2 -1
  20. package/apps/agents-server/src/tools/agentGoalChatTimeoutToolFunctions.ts +24 -2
  21. package/apps/agents-server/src/tools/agentGoalChatTimeoutTools.ts +5 -4
  22. package/apps/agents-server/src/utils/agentGoalChat/agentGoalChatPlannedMessageActions.ts +38 -9
  23. package/apps/agents-server/src/utils/agentGoalChat/createAgentGoalChatNoteContent.ts +15 -6
  24. package/apps/agents-server/src/utils/agentProjects/listAgentProjectNames.ts +35 -0
  25. package/apps/agents-server/src/utils/agentProjects/listAgentProjects.ts +6 -23
  26. package/apps/agents-server/src/utils/agentProjects/resolveAgentChatProjectReferences.ts +25 -0
  27. package/apps/agents-server/src/utils/chatMessageChips/createAnsweredMessageChipToolCalls.ts +37 -0
  28. package/apps/agents-server/src/utils/chatMessageChips/createPlannedMessageChipToolCalls.ts +39 -0
  29. package/apps/agents-server/src/utils/chatMessageChips/createTouchedProjectChipToolCalls.ts +75 -0
  30. package/apps/agents-server/src/utils/email/agentEmailAddress.ts +14 -0
  31. package/apps/agents-server/src/utils/email/sendEmailThroughStalwart.ts +2 -1
  32. package/apps/agents-server/src/utils/localChatRunner/applyLocalAgentPlannedMessageCommands.ts +212 -0
  33. package/apps/agents-server/src/utils/localChatRunner/prepareLocalAgentPlannedMessagesSidecar.ts +66 -0
  34. package/apps/agents-server/src/utils/localChatRunner/processLocalUserChatJob.ts +110 -22
  35. package/apps/agents-server/src/utils/userChat/persistUserChatJobTerminalState.ts +5 -0
  36. package/apps/agents-server/src/utils/userChat/userChatJobRunReport.ts +30 -0
  37. package/apps/agents-server/src/utils/userChatTimeout/createTimeoutWakeUpMessage.ts +9 -1
  38. package/apps/agents-server/src/utils/userChatTimeout/userChatTimeoutStore/repeatFiredUserChatTimeout.ts +74 -0
  39. package/apps/agents-server/src/utils/userChatTimeout/userChatTimeoutStore.ts +1 -0
  40. package/apps/agents-server/src/utils/userChatTimeout/userChatTimeoutWorker.ts +63 -54
  41. package/esm/index.es.js +1042 -194
  42. package/esm/index.es.js.map +1 -1
  43. package/esm/scripts/run-agent-messages/messages/buildAgentGoalChatPromptSection.d.ts +22 -4
  44. package/esm/scripts/run-agent-messages/messages/buildAgentMessagePrompt.d.ts +3 -3
  45. package/esm/scripts/run-agent-messages/messages/loadAgentPlannedMessagesSidecar.d.ts +10 -0
  46. package/esm/scripts/run-agent-messages/messages/resolveTouchedAgentProjects.d.ts +15 -0
  47. package/esm/scripts/run-codex-prompts/common/runGoScript/$spawnLoggedBashScript.d.ts +25 -0
  48. package/esm/scripts/run-codex-prompts/common/runGoScript/$terminateLoggedBashProcessTree.d.ts +11 -0
  49. package/esm/scripts/run-codex-prompts/common/runGoScript/scriptExecutionLog.d.ts +4 -0
  50. package/esm/src/book-2.0/agent-source/AgentSourceParseResult.d.ts +5 -0
  51. package/esm/src/book-2.0/agent-source/ParsedUnknownCommitment.d.ts +26 -0
  52. package/esm/src/book-2.0/agent-source/createAgentModelRequirementsWithCommitments.unknown.test.d.ts +1 -0
  53. package/esm/src/book-2.0/agent-source/getBookCommitmentLineType.d.ts +19 -0
  54. package/esm/src/book-2.0/agent-source/parseAgentSourceWithCommitments.unknown.test.d.ts +1 -0
  55. package/esm/src/book-3.0/AgentMessageRunReport.d.ts +28 -0
  56. package/esm/src/book-3.0/AgentPlannedMessagesSidecar.d.ts +94 -0
  57. package/esm/src/book-3.0/AgentPlannedMessagesSidecar.test.d.ts +1 -0
  58. package/esm/src/book-3.0/createAgentMessageSidecarBaseName.d.ts +13 -0
  59. package/esm/src/book-components/BookEditor/createUnknownCommitmentDiagnostics.d.ts +28 -0
  60. package/esm/src/book-components/BookEditor/createUnknownCommitmentDiagnostics.test.d.ts +1 -0
  61. package/esm/src/book-components/Chat/utils/agentProjectToolCall.d.ts +46 -0
  62. package/esm/src/book-components/Chat/utils/timeoutToolCallPresentation.d.ts +24 -0
  63. package/esm/src/cli/cli-commands/common/harness/$checkHarnessInstallation.d.ts +3 -3
  64. package/esm/src/cli/cli-commands/common/harness/$checkHarnessInstallation.test.d.ts +1 -0
  65. package/esm/src/cli/cli-commands/common/harness/$ensureHarnessInstallations.d.ts +3 -3
  66. package/esm/src/cli/cli-commands/common/harness/HarnessInstallationStatus.d.ts +3 -2
  67. package/esm/src/cli/cli-commands/common/harnessUpdateCliOptions.d.ts +29 -0
  68. package/esm/src/cli/cli-commands/common/harnessUpdateCliOptions.test.d.ts +1 -0
  69. package/esm/src/executables/platforms/locateAppOnWindows.d.ts +1 -1
  70. package/esm/src/executables/platforms/locateAppOnWindows.test.d.ts +1 -0
  71. package/esm/src/utils/agent-message-runtime/parseAgentMessageRuntimeLogEvents.d.ts +59 -0
  72. package/esm/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.d.ts +19 -0
  73. package/esm/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.test.d.ts +1 -0
  74. package/esm/src/version.d.ts +1 -1
  75. package/package.json +1 -1
  76. package/src/book-2.0/agent-source/AgentSourceParseResult.ts +6 -0
  77. package/src/book-2.0/agent-source/ParsedUnknownCommitment.ts +29 -0
  78. package/src/book-2.0/agent-source/createAgentModelRequirementsWithCommitments/augmentAgentModelRequirementsFromSource.ts +38 -1
  79. package/src/book-2.0/agent-source/getBookCommitmentLineType.ts +36 -0
  80. package/src/book-2.0/agent-source/parseAgentSourceWithCommitments.ts +128 -61
  81. package/src/book-3.0/AgentMessageRunReport.ts +76 -0
  82. package/src/book-3.0/AgentPlannedMessagesSidecar.ts +229 -0
  83. package/src/book-3.0/AgentTeamConversationWorkspace.ts +3 -5
  84. package/src/book-3.0/createAgentMessageSidecarBaseName.ts +20 -0
  85. package/src/book-components/BookEditor/BookEditorMonaco.tsx +6 -1
  86. package/src/book-components/BookEditor/BookEditorMonacoTokenization.ts +3 -1
  87. package/src/book-components/BookEditor/createUnknownCommitmentDiagnostics.ts +50 -0
  88. package/src/book-components/Chat/Chat/Chat.module.css +6 -1
  89. package/src/book-components/Chat/Chat/renderToolCallDetails.tsx +1 -12
  90. package/src/book-components/Chat/utils/agentProjectToolCall.ts +74 -0
  91. package/src/book-components/Chat/utils/getToolCallChipletInfo.ts +28 -0
  92. package/src/book-components/Chat/utils/timeoutToolCallPresentation.ts +23 -3
  93. package/src/cli/cli-commands/coder/boilerplateTemplates.ts +3 -15
  94. package/src/cli/cli-commands/coder/init.ts +11 -2
  95. package/src/cli/cli-commands/coder/ping.ts +11 -1
  96. package/src/cli/cli-commands/coder/run.ts +11 -2
  97. package/src/cli/cli-commands/coder/server.ts +11 -2
  98. package/src/cli/cli-commands/common/harness/$applyHarnessInstallationStatus.ts +5 -0
  99. package/src/cli/cli-commands/common/harness/$checkHarnessInstallation.ts +17 -5
  100. package/src/cli/cli-commands/common/harness/$ensureHarnessInstallations.ts +6 -3
  101. package/src/cli/cli-commands/common/harness/HarnessInstallationStatus.ts +3 -2
  102. package/src/cli/cli-commands/common/harnessUpdateCliOptions.ts +49 -0
  103. package/src/executables/platforms/locateAppOnWindows.ts +12 -9
  104. package/src/other/templates/getTemplatesPipelineCollection.ts +740 -860
  105. package/src/utils/agent-message-runtime/parseAgentMessageRuntimeLogEvents.ts +105 -0
  106. package/src/utils/agent-message-runtime/resolveAgentMessageRuntimeActivity.ts +6 -54
  107. package/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.ts +147 -0
  108. package/src/version.ts +2 -2
  109. package/src/versions.txt +2 -1
  110. package/umd/index.umd.js +1041 -193
  111. package/umd/index.umd.js.map +1 -1
  112. package/umd/scripts/run-agent-messages/messages/buildAgentGoalChatPromptSection.d.ts +22 -4
  113. package/umd/scripts/run-agent-messages/messages/buildAgentMessagePrompt.d.ts +3 -3
  114. package/umd/scripts/run-agent-messages/messages/loadAgentPlannedMessagesSidecar.d.ts +10 -0
  115. package/umd/scripts/run-agent-messages/messages/resolveTouchedAgentProjects.d.ts +15 -0
  116. package/umd/scripts/run-codex-prompts/common/runGoScript/$spawnLoggedBashScript.d.ts +25 -0
  117. package/umd/scripts/run-codex-prompts/common/runGoScript/$terminateLoggedBashProcessTree.d.ts +11 -0
  118. package/umd/scripts/run-codex-prompts/common/runGoScript/scriptExecutionLog.d.ts +4 -0
  119. package/umd/src/book-2.0/agent-source/AgentSourceParseResult.d.ts +5 -0
  120. package/umd/src/book-2.0/agent-source/ParsedUnknownCommitment.d.ts +26 -0
  121. package/umd/src/book-2.0/agent-source/createAgentModelRequirementsWithCommitments.unknown.test.d.ts +1 -0
  122. package/umd/src/book-2.0/agent-source/getBookCommitmentLineType.d.ts +19 -0
  123. package/umd/src/book-2.0/agent-source/parseAgentSourceWithCommitments.unknown.test.d.ts +1 -0
  124. package/umd/src/book-3.0/AgentMessageRunReport.d.ts +28 -0
  125. package/umd/src/book-3.0/AgentPlannedMessagesSidecar.d.ts +94 -0
  126. package/umd/src/book-3.0/AgentPlannedMessagesSidecar.test.d.ts +1 -0
  127. package/umd/src/book-3.0/createAgentMessageSidecarBaseName.d.ts +13 -0
  128. package/umd/src/book-components/BookEditor/createUnknownCommitmentDiagnostics.d.ts +28 -0
  129. package/umd/src/book-components/BookEditor/createUnknownCommitmentDiagnostics.test.d.ts +1 -0
  130. package/umd/src/book-components/Chat/utils/agentProjectToolCall.d.ts +46 -0
  131. package/umd/src/book-components/Chat/utils/timeoutToolCallPresentation.d.ts +24 -0
  132. package/umd/src/cli/cli-commands/common/harness/$checkHarnessInstallation.d.ts +3 -3
  133. package/umd/src/cli/cli-commands/common/harness/$checkHarnessInstallation.test.d.ts +1 -0
  134. package/umd/src/cli/cli-commands/common/harness/$ensureHarnessInstallations.d.ts +3 -3
  135. package/umd/src/cli/cli-commands/common/harness/HarnessInstallationStatus.d.ts +3 -2
  136. package/umd/src/cli/cli-commands/common/harnessUpdateCliOptions.d.ts +29 -0
  137. package/umd/src/cli/cli-commands/common/harnessUpdateCliOptions.test.d.ts +1 -0
  138. package/umd/src/executables/platforms/locateAppOnWindows.d.ts +1 -1
  139. package/umd/src/executables/platforms/locateAppOnWindows.test.d.ts +1 -0
  140. package/umd/src/utils/agent-message-runtime/parseAgentMessageRuntimeLogEvents.d.ts +59 -0
  141. package/umd/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.d.ts +19 -0
  142. package/umd/src/utils/agent-message-runtime/resolveAgentMessageTouchedProjectNames.test.d.ts +1 -0
  143. package/umd/src/version.d.ts +1 -1
  144. package/esm/apps/agents-server/src/utils/agentGoalChat/agentGoalChatConstants.d.ts +0 -12
  145. package/umd/apps/agents-server/src/utils/agentGoalChat/agentGoalChatConstants.d.ts +0 -12
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import Link from 'next/link';
3
4
  import { useEffect, useMemo, useState } from 'react';
4
5
  import { useServerLanguage } from '../../../components/ServerLanguage/ServerLanguageProvider';
5
6
  import { Card } from '../../../components/Homepage/Card';
@@ -175,7 +176,13 @@ export function MessagesClient() {
175
176
  Inspect all inbound and outbound messages and their statuses.
176
177
  </p>
177
178
  </div>
178
- <div className="flex items-end gap-4 text-sm text-gray-500 md:text-right">
179
+ <div className="flex flex-wrap items-end gap-4 text-sm text-gray-500 md:text-right">
180
+ <Link
181
+ href="/admin/messages/send-email"
182
+ className="rounded-md border border-blue-600 px-3 py-2 text-sm font-medium text-blue-700 hover:bg-blue-50"
183
+ >
184
+ Send test email
185
+ </Link>
179
186
  <div>
180
187
  <div className="text-xl font-semibold text-gray-900">{total.toLocaleString()}</div>
181
188
  <div className="text-xs uppercase tracking-wide text-gray-400">Total messages</div>
@@ -1,17 +1,131 @@
1
1
  'use client';
2
2
 
3
- import { useState } from 'react';
3
+ import { type FormEvent, useState } from 'react';
4
4
  import { assertsError } from '../../../../../../../src/errors/assertsError';
5
5
  import { sendEmailAction } from './actions';
6
6
 
7
7
  /**
8
- * Handles send email client.
8
+ * Existing agent address offered as a one-click form prefill.
9
9
  */
10
- export function SendEmailClient() {
10
+ export type EmailTestingAgentOption = {
11
+ readonly id: string;
12
+ readonly label: string;
13
+ readonly address: string;
14
+ };
15
+
16
+ /**
17
+ * Props accepted by the email testing client.
18
+ */
19
+ type SendEmailClientProps = {
20
+ readonly currentServerDomain: string;
21
+ readonly isGlobalAdmin: boolean;
22
+ readonly agentEmailOptions: ReadonlyArray<EmailTestingAgentOption>;
23
+ };
24
+
25
+ /**
26
+ * Props accepted by an address editor with optional agent-address prefills.
27
+ */
28
+ type EmailTestingAddressInputProps = {
29
+ readonly label: string;
30
+ readonly name: 'from' | 'to';
31
+ readonly value: string;
32
+ readonly placeholder: string;
33
+ readonly helperText: string;
34
+ readonly agentEmailOptions: ReadonlyArray<EmailTestingAgentOption>;
35
+ readonly isDisabled: boolean;
36
+ readonly onValueChange: (value: string) => void;
37
+ };
38
+
39
+ /**
40
+ * Renders one arbitrary email-address input and lets administrators pre-fill it from an existing agent.
41
+ */
42
+ function EmailTestingAddressInput({
43
+ label,
44
+ name,
45
+ value,
46
+ placeholder,
47
+ helperText,
48
+ agentEmailOptions,
49
+ isDisabled,
50
+ onValueChange,
51
+ }: EmailTestingAddressInputProps) {
52
+ const helperTextId = `${name}-helper-text`;
53
+
54
+ return (
55
+ <div>
56
+ <div className="flex flex-wrap items-center justify-between gap-2">
57
+ <label htmlFor={name} className="block text-sm font-medium text-gray-700">
58
+ {label}
59
+ </label>
60
+ {agentEmailOptions.length > 0 ? (
61
+ <select
62
+ aria-label={`Pre-fill ${label.toLowerCase()} with an existing agent address`}
63
+ value=""
64
+ onChange={(event) => {
65
+ if (event.target.value) {
66
+ onValueChange(event.target.value);
67
+ }
68
+ }}
69
+ disabled={isDisabled}
70
+ className="rounded-md border border-gray-300 bg-white px-2 py-1 text-xs text-gray-700"
71
+ >
72
+ <option value="">Pre-fill from an agent</option>
73
+ {agentEmailOptions.map((agent) => (
74
+ <option key={agent.id} value={agent.address}>
75
+ {agent.label} — {agent.address}
76
+ </option>
77
+ ))}
78
+ </select>
79
+ ) : null}
80
+ </div>
81
+ <input
82
+ id={name}
83
+ name={name}
84
+ type="text"
85
+ value={value}
86
+ onChange={(event) => onValueChange(event.target.value)}
87
+ placeholder={placeholder}
88
+ aria-describedby={helperTextId}
89
+ required
90
+ disabled={isDisabled}
91
+ className="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 disabled:bg-gray-100"
92
+ />
93
+ <p id={helperTextId} className="mt-1 text-xs text-gray-500">
94
+ {helperText}
95
+ </p>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ /**
101
+ * Explains the domain scope available to the current administrator.
102
+ */
103
+ function getEmailTestingScopeMessage(currentServerDomain: string, isGlobalAdmin: boolean): string {
104
+ if (isGlobalAdmin) {
105
+ return 'As the superadmin, you can use a sender address from any domain.';
106
+ }
107
+
108
+ return `You can use any recipient address, but the sender must use ${currentServerDomain}.`;
109
+ }
110
+
111
+ /**
112
+ * Renders and submits the administration email testing form.
113
+ */
114
+ export function SendEmailClient({
115
+ currentServerDomain,
116
+ isGlobalAdmin,
117
+ agentEmailOptions,
118
+ }: SendEmailClientProps) {
11
119
  const [status, setStatus] = useState<'IDLE' | 'LOADING' | 'SUCCESS' | 'ERROR'>('IDLE');
12
120
  const [errorMessage, setErrorMessage] = useState<string | null>(null);
121
+ const [from, setFrom] = useState('');
122
+ const [to, setTo] = useState('');
123
+ const isLoading = status === 'LOADING';
13
124
 
14
- const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
125
+ /**
126
+ * Sends the currently composed test email and displays the server-side outcome.
127
+ */
128
+ const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
15
129
  event.preventDefault();
16
130
  setStatus('LOADING');
17
131
  setErrorMessage(null);
@@ -31,77 +145,93 @@ export function SendEmailClient() {
31
145
 
32
146
  return (
33
147
  <div className="container mx-auto p-4">
34
- <h1 className="text-2xl font-bold mb-4">Send Email (Test)</h1>
35
-
36
- {status === 'SUCCESS' && (
37
- <div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
38
- Email sent successfully!
148
+ <div className="mx-auto mt-20 max-w-2xl space-y-6">
149
+ <div>
150
+ <h1 className="text-3xl font-light text-gray-900">Email testing</h1>
151
+ <p className="mt-1 text-sm text-gray-500">
152
+ Send a manually composed email through this server&apos;s email service.
153
+ </p>
154
+ <p className="mt-2 rounded-md bg-blue-50 px-3 py-2 text-sm text-blue-950">
155
+ {getEmailTestingScopeMessage(currentServerDomain, isGlobalAdmin)}
156
+ </p>
39
157
  </div>
40
- )}
41
158
 
42
- {status === 'ERROR' && (
43
- <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
44
- Error: {errorMessage}
45
- </div>
46
- )}
159
+ {status === 'SUCCESS' && (
160
+ <div className="rounded border border-green-400 bg-green-100 px-4 py-3 text-green-700">
161
+ Email sent successfully!
162
+ </div>
163
+ )}
47
164
 
48
- <form onSubmit={handleSubmit} className="space-y-4 max-w-lg">
49
- <div>
50
- <label className="block text-sm font-medium text-gray-700">From</label>
51
- <input
165
+ {status === 'ERROR' && (
166
+ <div className="rounded border border-red-400 bg-red-100 px-4 py-3 text-red-700">
167
+ Error: {errorMessage}
168
+ </div>
169
+ )}
170
+
171
+ <form onSubmit={handleSubmit} className="space-y-4 rounded-xl border border-gray-200 bg-white p-5 shadow-sm">
172
+ <EmailTestingAddressInput
173
+ label="From"
52
174
  name="from"
53
- type="text"
54
- defaultValue="Test Promptbook <test@ptbk.io>"
55
- required
56
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 border p-2"
175
+ value={from}
176
+ onValueChange={setFrom}
177
+ placeholder={`Sender <sender@${currentServerDomain}>`}
178
+ helperText="Enter any valid sender address, or pre-fill one of this server's agents."
179
+ agentEmailOptions={agentEmailOptions}
180
+ isDisabled={isLoading}
57
181
  />
58
- </div>
59
182
 
60
- <div>
61
- <label className="block text-sm font-medium text-gray-700">To</label>
62
- <input
183
+ <EmailTestingAddressInput
184
+ label="To"
63
185
  name="to"
64
- type="text"
186
+ value={to}
187
+ onValueChange={setTo}
65
188
  placeholder="recipient@example.com"
66
- defaultValue="Pavol Hejný <pavol@ptbk.io>"
67
- required
68
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 border p-2"
189
+ helperText="Separate multiple addresses with commas."
190
+ agentEmailOptions={agentEmailOptions}
191
+ isDisabled={isLoading}
69
192
  />
70
- <p className="text-xs text-gray-500 mt-1">Separate multiple addresses with commas</p>
71
- </div>
72
193
 
73
- <div>
74
- <label className="block text-sm font-medium text-gray-700">Subject</label>
75
- <input
76
- name="subject"
77
- type="text"
78
- placeholder="Test Email"
79
- defaultValue="Test Email"
80
- required
81
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 border p-2"
82
- />
83
- </div>
194
+ <div>
195
+ <label htmlFor="subject" className="block text-sm font-medium text-gray-700">
196
+ Subject
197
+ </label>
198
+ <input
199
+ id="subject"
200
+ name="subject"
201
+ type="text"
202
+ placeholder="Test email"
203
+ defaultValue="Test email"
204
+ required
205
+ disabled={isLoading}
206
+ className="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 disabled:bg-gray-100"
207
+ />
208
+ </div>
84
209
 
85
- <div>
86
- <label className="block text-sm font-medium text-gray-700">Body</label>
87
- <textarea
88
- name="body"
89
- rows={6}
90
- placeholder="Hello, this is a test email."
91
- defaultValue="Hello, this is a test email."
92
- required
93
- className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 border p-2"
94
- />
95
- </div>
210
+ <div>
211
+ <label htmlFor="body" className="block text-sm font-medium text-gray-700">
212
+ Body
213
+ </label>
214
+ <textarea
215
+ id="body"
216
+ name="body"
217
+ rows={6}
218
+ placeholder="Hello, this is a test email."
219
+ defaultValue="Hello, this is a test email."
220
+ required
221
+ disabled={isLoading}
222
+ className="mt-1 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 disabled:bg-gray-100"
223
+ />
224
+ </div>
96
225
 
97
- <button
98
- type="submit"
99
- disabled={status === 'LOADING'}
100
- className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
101
- >
102
- {status === 'LOADING' ? 'Sending...' : 'Send Email'}
103
- </button>
104
- </form>
226
+ <button
227
+ type="submit"
228
+ disabled={isLoading}
229
+ className="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700 disabled:opacity-50"
230
+ >
231
+ {isLoading ? 'Sending...' : 'Send email'}
232
+ </button>
233
+ </form>
234
+ </div>
105
235
  </div>
106
236
  );
107
237
  }
@@ -1,38 +1,35 @@
1
1
  'use server';
2
2
 
3
- import type { string_email, string_emails, string_html } from '@promptbook-local/types';
4
- import { parseEmailAddress } from '../../../../message-providers/email/_common/utils/parseEmailAddress';
5
- import { parseEmailAddresses } from '../../../../message-providers/email/_common/utils/parseEmailAddresses';
6
- import { stringifyEmailAddress } from '../../../../message-providers/email/_common/utils/stringifyEmailAddress';
7
- import { sendMessage } from '../../../../utils/messages/sendMessage';
3
+ import { NotAllowed } from '../../../../../../../src/errors/NotAllowed';
4
+ import { spaceTrim } from '../../../../../../../src/utils/organization/spaceTrim';
5
+ import { sendEmailThroughStalwart } from '../../../../utils/email/sendEmailThroughStalwart';
6
+ import {
7
+ assertEmailTestingSenderDomainAllowed,
8
+ createEmailTestingOutboundEmail,
9
+ readEmailTestingFormValues,
10
+ } from './emailTesting';
11
+ import { getEmailTestingAccessContext } from './emailTestingAccess';
8
12
 
9
13
  /**
10
- * Handles send email action.
14
+ * Sends an administrator-composed test email through the bundled Stalwart email service.
11
15
  */
12
- export async function sendEmailAction(formData: FormData) {
13
- const from = formData.get('from') as string;
14
- const to = formData.get('to') as string;
15
- const subject = formData.get('subject') as string;
16
- const body = formData.get('body') as string;
16
+ export async function sendEmailAction(formData: FormData): Promise<void> {
17
+ const accessContext = await getEmailTestingAccessContext();
17
18
 
18
- if (!from || !to || !subject || !body) {
19
- throw new Error('All fields are required');
19
+ if (!accessContext) {
20
+ throw new NotAllowed(
21
+ spaceTrim(`
22
+ You are not allowed to send test emails.
23
+ `),
24
+ );
20
25
  }
21
26
 
22
- const sender = stringifyEmailAddress(parseEmailAddress(from as string_email));
23
- const recipients = parseEmailAddresses(to as string_emails).map(stringifyEmailAddress);
24
-
25
- await sendMessage({
26
- channel: 'EMAIL',
27
- direction: 'OUTBOUND',
28
- sender,
29
- recipients,
30
- subject,
31
- content: body as string_html,
32
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
- threadId: crypto.randomUUID() as any,
34
- cc: [],
35
- attachments: [],
36
- metadata: {},
27
+ const email = createEmailTestingOutboundEmail(readEmailTestingFormValues(formData));
28
+ assertEmailTestingSenderDomainAllowed({
29
+ sender: email.sender,
30
+ currentServerDomain: accessContext.currentServerDomain,
31
+ isGlobalAdmin: accessContext.isGlobalAdmin,
37
32
  });
33
+
34
+ await sendEmailThroughStalwart(email);
38
35
  }
@@ -0,0 +1,153 @@
1
+ import type { string_email, string_emails, string_markdown } from '@promptbook-local/types';
2
+ import { NotAllowed } from '../../../../../../../src/errors/NotAllowed';
3
+ import { ParseError } from '../../../../../../../src/errors/ParseError';
4
+ import { spaceTrim } from '../../../../../../../src/utils/organization/spaceTrim';
5
+ import type { OutboundEmail } from '../../../../message-providers/email/_common/Email';
6
+ import { parseEmailAddress } from '../../../../message-providers/email/_common/utils/parseEmailAddress';
7
+ import { parseEmailAddresses } from '../../../../message-providers/email/_common/utils/parseEmailAddresses';
8
+ import { stringifyEmailAddress } from '../../../../message-providers/email/_common/utils/stringifyEmailAddress';
9
+ import { getEmailAddressDomain, normalizeEmailDomain } from '../../../../utils/email/agentEmailAddress';
10
+
11
+ /**
12
+ * Raw values submitted through the administration email testing form.
13
+ */
14
+ export type EmailTestingFormValues = {
15
+ readonly from: string;
16
+ readonly to: string;
17
+ readonly subject: string;
18
+ readonly body: string;
19
+ };
20
+
21
+ /**
22
+ * Reads and validates the required values of the administration email testing form.
23
+ */
24
+ export function readEmailTestingFormValues(formData: FormData): EmailTestingFormValues {
25
+ return {
26
+ from: readRequiredEmailTestingFormText(formData, 'from'),
27
+ to: readRequiredEmailTestingFormText(formData, 'to'),
28
+ subject: readRequiredEmailTestingFormText(formData, 'subject'),
29
+ body: readRequiredEmailTestingFormText(formData, 'body'),
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Creates the normalized outbound email persisted and delivered by the testing tool.
35
+ */
36
+ export function createEmailTestingOutboundEmail(formValues: EmailTestingFormValues): OutboundEmail {
37
+ const sender = parseEmailTestingSender(formValues.from);
38
+ const recipients = parseEmailTestingRecipients(formValues.to);
39
+
40
+ return {
41
+ channel: 'EMAIL',
42
+ direction: 'OUTBOUND',
43
+ sender,
44
+ recipients,
45
+ subject: formValues.subject,
46
+ content: formValues.body as string_markdown,
47
+ threadId: crypto.randomUUID(),
48
+ cc: [],
49
+ attachments: [],
50
+ metadata: {},
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Ensures that a normal administrator sends test mail only from the current server's domain.
56
+ */
57
+ export function assertEmailTestingSenderDomainAllowed(options: {
58
+ readonly sender: string_email;
59
+ readonly currentServerDomain: string;
60
+ readonly isGlobalAdmin: boolean;
61
+ }): void {
62
+ if (options.isGlobalAdmin) {
63
+ return;
64
+ }
65
+
66
+ const senderDomain = getEmailAddressDomain(options.sender);
67
+ const currentServerDomain = normalizeEmailDomain(options.currentServerDomain);
68
+
69
+ if (senderDomain === currentServerDomain) {
70
+ return;
71
+ }
72
+
73
+ throw new NotAllowed(
74
+ spaceTrim(`
75
+ You are only allowed to send test emails from the current server's domain.
76
+
77
+ **Current server domain:** \`${currentServerDomain}\`
78
+ **Sender domain:** \`${senderDomain}\`
79
+ `),
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Reads one required string value from the submitted email testing form.
85
+ */
86
+ function readRequiredEmailTestingFormText(formData: FormData, fieldName: string): string {
87
+ const value = formData.get(fieldName);
88
+
89
+ if (typeof value !== 'string' || !value.trim()) {
90
+ throw new ParseError(
91
+ spaceTrim(`
92
+ The \`${fieldName}\` field is required to send a test email.
93
+ `),
94
+ );
95
+ }
96
+
97
+ return value;
98
+ }
99
+
100
+ /**
101
+ * Parses one sender while converting parser failures into a user-facing branded error.
102
+ */
103
+ function parseEmailTestingSender(value: string): string_email {
104
+ try {
105
+ return stringifyEmailAddress(parseEmailAddress(value.trim() as string_email));
106
+ } catch (error) {
107
+ throw new ParseError(
108
+ spaceTrim(`
109
+ The \`from\` email address is invalid.
110
+
111
+ **Cause:** \`${getEmailTestingErrorMessage(error)}\`
112
+ `),
113
+ );
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Parses one or more comma-separated recipients into transport-ready addresses.
119
+ */
120
+ function parseEmailTestingRecipients(value: string): string_email[] {
121
+ let recipients: string_email[];
122
+
123
+ try {
124
+ recipients = parseEmailAddresses(value.trim() as string_emails).map(stringifyEmailAddress);
125
+ } catch (error) {
126
+ throw new ParseError(
127
+ spaceTrim(`
128
+ The \`to\` email address is invalid.
129
+
130
+ **Cause:** \`${getEmailTestingErrorMessage(error)}\`
131
+ `),
132
+ );
133
+ }
134
+
135
+ if (recipients.length > 0) {
136
+ return recipients;
137
+ }
138
+
139
+ throw new ParseError(
140
+ spaceTrim(`
141
+ The \`to\` email address is invalid.
142
+
143
+ **Cause:** \`No recipient email addresses were provided.\`
144
+ `),
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Gets a safe textual reason from an arbitrary parsing failure.
150
+ */
151
+ function getEmailTestingErrorMessage(error: unknown): string {
152
+ return error instanceof Error ? error.message : String(error);
153
+ }
@@ -0,0 +1,30 @@
1
+ import { $provideServer } from '../../../../tools/$provideServer';
2
+ import { isUserAdmin } from '../../../../utils/isUserAdmin';
3
+ import { isUserGlobalAdmin } from '../../../../utils/isUserGlobalAdmin';
4
+ import { normalizeEmailDomain } from '../../../../utils/email/agentEmailAddress';
5
+
6
+ /**
7
+ * Authorization and server scope available to the administration email testing tool.
8
+ */
9
+ export type EmailTestingAccessContext = {
10
+ readonly currentServerDomain: string;
11
+ readonly isGlobalAdmin: boolean;
12
+ };
13
+
14
+ /**
15
+ * Resolves access to the email testing tool for either a current-server admin or the VPS superadmin.
16
+ */
17
+ export async function getEmailTestingAccessContext(): Promise<EmailTestingAccessContext | null> {
18
+ const [isAdmin, isGlobalAdmin] = await Promise.all([isUserAdmin(), isUserGlobalAdmin()]);
19
+
20
+ if (!isAdmin && !isGlobalAdmin) {
21
+ return null;
22
+ }
23
+
24
+ const server = await $provideServer();
25
+
26
+ return {
27
+ currentServerDomain: normalizeEmailDomain(server.publicUrl.hostname),
28
+ isGlobalAdmin,
29
+ };
30
+ }
@@ -1,16 +1,29 @@
1
1
  import { ForbiddenPage } from '../../../../components/ForbiddenPage/ForbiddenPage';
2
- import { isUserAdmin } from '../../../../utils/isUserAdmin';
2
+ import { listAgentEmailIdentities } from '../../../../utils/email/listAgentEmailIdentities';
3
3
  import { SendEmailClient } from './SendEmailClient';
4
+ import { getEmailTestingAccessContext } from './emailTestingAccess';
4
5
 
5
6
  /**
6
- * Handles admin send email page.
7
+ * Renders the current server's email testing tool.
7
8
  */
8
9
  export default async function AdminSendEmailPage() {
9
- const isAdmin = await isUserAdmin();
10
+ const accessContext = await getEmailTestingAccessContext();
10
11
 
11
- if (!isAdmin) {
12
+ if (!accessContext) {
12
13
  return <ForbiddenPage />;
13
14
  }
14
15
 
15
- return <SendEmailClient />;
16
+ const agentEmailOptions = (await listAgentEmailIdentities(accessContext.currentServerDomain)).map((agent) => ({
17
+ id: agent.permanentId,
18
+ label: agent.displayName,
19
+ address: agent.preferredEmail,
20
+ }));
21
+
22
+ return (
23
+ <SendEmailClient
24
+ currentServerDomain={accessContext.currentServerDomain}
25
+ isGlobalAdmin={accessContext.isGlobalAdmin}
26
+ agentEmailOptions={agentEmailOptions}
27
+ />
28
+ );
16
29
  }
@@ -6,6 +6,7 @@ import { AgentChat, ChatMessage, useSendMessageToLlmChat } from '@promptbook-loc
6
6
  import { RemoteAgent } from '@promptbook-local/core';
7
7
  import { ClientVersionMismatchError } from '@promptbook-local/utils';
8
8
  import { useCallback, useEffect, useMemo, useState, type CSSProperties } from 'react';
9
+ import { AGENT_PROJECT_TOOL_CALL_NAME } from '../../../../../../src/book-components/Chat/utils/agentProjectToolCall';
9
10
  import { string_agent_url } from '../../../../../../src/types/typeAliases';
10
11
  import type { AgentProjectItemInfo } from '../../../components/AgentProjects/AgentProjectReferencesList';
11
12
  import { useAgentProjectMarkdownReferences } from '../../../components/AgentProjects/useAgentProjectMarkdownReferences';
@@ -144,10 +145,6 @@ export function AgentChatWrapper(props: AgentChatWrapperProps) {
144
145
 
145
146
  const { value: agent } = usePromise(agentPromise, [agentPromise]);
146
147
  const teamAgentProfiles = useTeamAgentProfiles(agent?.capabilities);
147
- const markdownInlineReferences = useAgentProjectMarkdownReferences({
148
- agentPermanentId: agentName,
149
- projects: projectReferences,
150
- });
151
148
 
152
149
  // Error state management
153
150
  const [currentError, setCurrentError] = useState<FriendlyErrorMessage | null>(null);
@@ -157,6 +154,11 @@ export function AgentChatWrapper(props: AgentChatWrapperProps) {
157
154
  const [githubAppStatus, setGithubAppStatus] = useState<GithubAppStatusResponse | null>(null);
158
155
  const [calendarOAuthStatus, setCalendarOAuthStatus] = useState<CalendarOAuthStatusResponse | null>(null);
159
156
  const [renderedMessages, setRenderedMessages] = useState<ReadonlyArray<ChatMessage>>([]);
157
+ const markdownInlineReferences = useAgentProjectMarkdownReferences({
158
+ agentPermanentId: agentName,
159
+ projects: projectReferences,
160
+ messages: renderedMessages,
161
+ });
160
162
  const currentAgentPermanentId = useMemo(() => {
161
163
  return typeof (agent as { permanentId?: unknown } | undefined)?.permanentId === 'string'
162
164
  ? ((agent as { permanentId?: string }).permanentId as string)
@@ -454,6 +456,7 @@ export function AgentChatWrapper(props: AgentChatWrapperProps) {
454
456
  project_delete_file: t('chat.toolTitle.projectFileDeleter'),
455
457
  project_create_branch: t('chat.toolTitle.projectBranchCreator'),
456
458
  project_create_pull_request: t('chat.toolTitle.projectPullRequestCreator'),
459
+ [AGENT_PROJECT_TOOL_CALL_NAME]: t('chat.toolTitle.agentProjectTouched'),
457
460
  }}
458
461
  feedbackMode={toChatComponentFeedbackMode(feedbackMode)}
459
462
  onFeedback={shouldEnableFeedback ? handleFeedback : undefined}