@chainingintention/pi-web-cn 1.202610.3 → 1.202610.4

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 (202) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +129 -122
  3. package/dist/cli.js.map +1 -1
  4. package/dist/client/assets/PiWebApp-CN3g7SqA.js +4362 -0
  5. package/dist/client/assets/external-DUGCSDhZ.js +2 -0
  6. package/dist/client/assets/index-JduZlbIF.js +2 -0
  7. package/dist/client/index.html +4 -4
  8. package/dist/client/manifest.webmanifest +2 -1
  9. package/dist/localization.js +24 -0
  10. package/dist/localization.js.map +1 -0
  11. package/dist/nativeServices/serviceDoctor.js +45 -44
  12. package/dist/nativeServices/serviceDoctor.js.map +1 -1
  13. package/dist/pi-packages/captains-log/dist/browser/index.js +46 -45
  14. package/dist/pi-packages/captains-log/dist/browser/markdown.js +2 -1
  15. package/dist/pi-packages/captains-log/dist/browser/panel.js +56 -55
  16. package/dist/pi-packages/captains-log/i18n.js +7 -0
  17. package/dist/pi-packages/relays/i18n.js +7 -0
  18. package/dist/pi-packages/relays/markdownDocument.js +2 -1
  19. package/dist/pi-packages/relays/pi-web-plugin.js +8 -6
  20. package/dist/pi-packages/relays/relaysPanelElement.js +24 -23
  21. package/dist/pi-web-plugins/files/browser/pi-web-plugin.js +124 -124
  22. package/dist/pi-web-plugins/git/browser/git-contract.js +13 -12
  23. package/dist/pi-web-plugins/git/browser/git-panel.js +25 -24
  24. package/dist/pi-web-plugins/git/browser/i18n.js +7 -0
  25. package/dist/pi-web-plugins/git/browser/pi-web-plugin.js +7 -3
  26. package/dist/pi-web-plugins/git/git-backend.js +10 -9
  27. package/dist/pi-web-plugins/git/i18n.js +7 -0
  28. package/dist/pi-web-plugins/git/server-plugin.js +21 -19
  29. package/dist/pi-web-plugins/info/i18n.js +7 -0
  30. package/dist/pi-web-plugins/info/infoInternals.js +57 -62
  31. package/dist/pi-web-plugins/info/pi-web-plugin.js +34 -36
  32. package/dist/pi-web-plugins/mermaid/browser/pi-web-plugin.js +3 -3
  33. package/dist/pi-web-plugins/terminal/browser/pi-web-plugin.js +55 -52
  34. package/dist/pi-web-plugins/terminal/i18n.js +7 -0
  35. package/dist/pi-web-plugins/terminal/server-plugin.js +23 -21
  36. package/dist/pi-web-plugins/terminal/terminalService.js +22 -21
  37. package/dist/pi-web-plugins/updates/i18n.js +7 -0
  38. package/dist/pi-web-plugins/updates/pi-web-plugin.js +58 -54
  39. package/dist/pi-web-plugins/updates/updatesLogic.js +21 -20
  40. package/dist/pi-web-plugins/workspace-tasks/config.js +12 -11
  41. package/dist/pi-web-plugins/workspace-tasks/i18n.js +7 -0
  42. package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +8 -6
  43. package/dist/pi-web-plugins/workspace-tasks/tasksPanelElement.js +20 -19
  44. package/dist/pi-web-plugins/workspace-tasks/workspaceTasksClient.js +12 -11
  45. package/dist/pi-web-plugins/zh-cn/auth.json +9 -0
  46. package/dist/pi-web-plugins/zh-cn/bundled.json +176 -0
  47. package/dist/pi-web-plugins/zh-cn/cli.json +171 -0
  48. package/dist/pi-web-plugins/zh-cn/client.json +219 -0
  49. package/dist/pi-web-plugins/zh-cn/components.json +1002 -0
  50. package/dist/pi-web-plugins/zh-cn/config-errors.json +172 -0
  51. package/dist/pi-web-plugins/zh-cn/files.json +41 -0
  52. package/dist/pi-web-plugins/zh-cn/machines.json +28 -0
  53. package/dist/pi-web-plugins/zh-cn/main.json +459 -0
  54. package/dist/pi-web-plugins/zh-cn/management-errors.json +23 -0
  55. package/dist/pi-web-plugins/zh-cn/manifest.webmanifest +25 -0
  56. package/dist/pi-web-plugins/zh-cn/metadata.json +3 -0
  57. package/dist/pi-web-plugins/zh-cn/package.json +10 -0
  58. package/dist/pi-web-plugins/zh-cn/pi-web-plugin.js +16 -0
  59. package/dist/pi-web-plugins/zh-cn/projects.json +6 -0
  60. package/dist/pi-web-plugins/zh-cn/provider-auth.json +17 -0
  61. package/dist/pi-web-plugins/zh-cn/routing.json +68 -0
  62. package/dist/pi-web-plugins/zh-cn/server-main.json +99 -0
  63. package/dist/pi-web-plugins/zh-cn/server.json +174 -0
  64. package/dist/piWebVersionReport.js +17 -16
  65. package/dist/piWebVersionReport.js.map +1 -1
  66. package/dist/plugin-api.d.ts +4 -1
  67. package/dist/pluginRecoveryCli.js +25 -24
  68. package/dist/pluginRecoveryCli.js.map +1 -1
  69. package/dist/server/app.js +43 -28
  70. package/dist/server/app.js.map +1 -1
  71. package/dist/server/audit/normalToolAuditCli.js +45 -43
  72. package/dist/server/audit/normalToolAuditCli.js.map +1 -1
  73. package/dist/server/configRoutes.js +39 -30
  74. package/dist/server/configRoutes.js.map +1 -1
  75. package/dist/server/diagnostics/nodePtyNativeModule.js +7 -6
  76. package/dist/server/diagnostics/nodePtyNativeModule.js.map +1 -1
  77. package/dist/server/diagnostics/nodePtySpawnHelper.js +21 -20
  78. package/dist/server/diagnostics/nodePtySpawnHelper.js.map +1 -1
  79. package/dist/server/knownAutoInstallPiPackages.js +2 -2
  80. package/dist/server/knownAutoInstallPiPackages.js.map +1 -1
  81. package/dist/server/knownPiPackages.js +2 -2
  82. package/dist/server/knownPiPackages.js.map +1 -1
  83. package/dist/server/machines/machineClient.js +8 -6
  84. package/dist/server/machines/machineClient.js.map +1 -1
  85. package/dist/server/machines/machinePluginProxyRoutes.js +11 -10
  86. package/dist/server/machines/machinePluginProxyRoutes.js.map +1 -1
  87. package/dist/server/machines/machineProxyRoutes.js +27 -29
  88. package/dist/server/machines/machineProxyRoutes.js.map +1 -1
  89. package/dist/server/machines/machineRoutes.js +10 -12
  90. package/dist/server/machines/machineRoutes.js.map +1 -1
  91. package/dist/server/machines/machineService.js +17 -18
  92. package/dist/server/machines/machineService.js.map +1 -1
  93. package/dist/server/managementEmbed.js +26 -25
  94. package/dist/server/managementEmbed.js.map +1 -1
  95. package/dist/server/normalAuth.js +29 -26
  96. package/dist/server/normalAuth.js.map +1 -1
  97. package/dist/server/passwordVerifier.js +2 -1
  98. package/dist/server/passwordVerifier.js.map +1 -1
  99. package/dist/server/piPackageService.js +7 -5
  100. package/dist/server/piPackageService.js.map +1 -1
  101. package/dist/server/piWebPluginService.js +27 -11
  102. package/dist/server/piWebPluginService.js.map +1 -1
  103. package/dist/server/piWebStatus.js +16 -14
  104. package/dist/server/piWebStatus.js.map +1 -1
  105. package/dist/server/plugins/serverPluginRuntime.js +4 -2
  106. package/dist/server/plugins/serverPluginRuntime.js.map +1 -1
  107. package/dist/server/projects/projectService.js +4 -3
  108. package/dist/server/projects/projectService.js.map +1 -1
  109. package/dist/server/sessiond/projectMutationRoutes.js +5 -4
  110. package/dist/server/sessiond/projectMutationRoutes.js.map +1 -1
  111. package/dist/server/sessiond/sessionProxyRoutes.js +11 -8
  112. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -1
  113. package/dist/server/sessiond/workspaceCatalogRoutes.js +5 -3
  114. package/dist/server/sessiond/workspaceCatalogRoutes.js.map +1 -1
  115. package/dist/server/sessiond.js +8 -3
  116. package/dist/server/sessiond.js.map +1 -1
  117. package/dist/server/sessions/authRoutes.js +20 -19
  118. package/dist/server/sessions/authRoutes.js.map +1 -1
  119. package/dist/server/sessions/authService.js +13 -10
  120. package/dist/server/sessions/authService.js.map +1 -1
  121. package/dist/server/sessions/builtinCommands.js +28 -21
  122. package/dist/server/sessions/builtinCommands.js.map +1 -1
  123. package/dist/server/sessions/oauthLoginFlowService.js +17 -15
  124. package/dist/server/sessions/oauthLoginFlowService.js.map +1 -1
  125. package/dist/server/sessions/piSessionService.js +229 -208
  126. package/dist/server/sessions/piSessionService.js.map +1 -1
  127. package/dist/server/sessions/projectAuthService.js +5 -3
  128. package/dist/server/sessions/projectAuthService.js.map +1 -1
  129. package/dist/server/sessions/sessionArchiveMapping.js +8 -7
  130. package/dist/server/sessions/sessionArchiveMapping.js.map +1 -1
  131. package/dist/server/sessions/sessionCleanup.js +4 -3
  132. package/dist/server/sessions/sessionCleanup.js.map +1 -1
  133. package/dist/server/sessions/sessionCommandService.js +70 -67
  134. package/dist/server/sessions/sessionCommandService.js.map +1 -1
  135. package/dist/server/sessions/sessionNotificationStore.js +12 -11
  136. package/dist/server/sessions/sessionNotificationStore.js.map +1 -1
  137. package/dist/server/sessions/sessionRoutes.js +74 -72
  138. package/dist/server/sessions/sessionRoutes.js.map +1 -1
  139. package/dist/server/sessions/sessionTreeProjection.js +50 -49
  140. package/dist/server/sessions/sessionTreeProjection.js.map +1 -1
  141. package/dist/server/sessions/sessionUnreadStore.js +25 -2
  142. package/dist/server/sessions/sessionUnreadStore.js.map +1 -1
  143. package/dist/server/terminals/managementCommandRunService.js +13 -12
  144. package/dist/server/terminals/managementCommandRunService.js.map +1 -1
  145. package/dist/server/workbench/accessStateStore.js +9 -7
  146. package/dist/server/workbench/accessStateStore.js.map +1 -1
  147. package/dist/server/workbench/capabilitySearch.js +3 -2
  148. package/dist/server/workbench/capabilitySearch.js.map +1 -1
  149. package/dist/server/workbench/knowledgeSearch.js +8 -7
  150. package/dist/server/workbench/knowledgeSearch.js.map +1 -1
  151. package/dist/server/workbench/mcpClient.js +6 -5
  152. package/dist/server/workbench/mcpClient.js.map +1 -1
  153. package/dist/server/workbench/skillSync.js +4 -2
  154. package/dist/server/workbench/skillSync.js.map +1 -1
  155. package/dist/server/workbench/workbenchClient.js +8 -7
  156. package/dist/server/workbench/workbenchClient.js.map +1 -1
  157. package/dist/server/workbench/workbenchTools.js +8 -6
  158. package/dist/server/workbench/workbenchTools.js.map +1 -1
  159. package/dist/server/workingDirectory.js +3 -2
  160. package/dist/server/workingDirectory.js.map +1 -1
  161. package/dist/server/workspaceExplorerRoutes.js +16 -13
  162. package/dist/server/workspaceExplorerRoutes.js.map +1 -1
  163. package/dist/server/workspaces/fileContentService.js +11 -10
  164. package/dist/server/workspaces/fileContentService.js.map +1 -1
  165. package/dist/server/workspaces/fileOperationService.js +7 -6
  166. package/dist/server/workspaces/fileOperationService.js.map +1 -1
  167. package/dist/server/workspaces/filePreviewService.js +5 -4
  168. package/dist/server/workspaces/filePreviewService.js.map +1 -1
  169. package/dist/server/workspaces/fileSuggestions.js +2 -1
  170. package/dist/server/workspaces/fileSuggestions.js.map +1 -1
  171. package/dist/server/workspaces/pathAccessPolicy.js +24 -18
  172. package/dist/server/workspaces/pathAccessPolicy.js.map +1 -1
  173. package/dist/server/workspaces/pathSafety.js +6 -5
  174. package/dist/server/workspaces/pathSafety.js.map +1 -1
  175. package/dist/server/workspaces/projectPiWebConfig.js +3 -2
  176. package/dist/server/workspaces/projectPiWebConfig.js.map +1 -1
  177. package/dist/server/workspaces/workspaceRemovalService.js +4 -3
  178. package/dist/server/workspaces/workspaceRemovalService.js.map +1 -1
  179. package/dist/server/workspaces/workspaceRouteErrors.js +4 -2
  180. package/dist/server/workspaces/workspaceRouteErrors.js.map +1 -1
  181. package/dist/server/workspaces/workspaceUserError.js +18 -0
  182. package/dist/server/workspaces/workspaceUserError.js.map +1 -0
  183. package/dist/server-plugin-api.d.ts +3 -1
  184. package/dist/server-plugin-api.js.map +1 -1
  185. package/dist/serverPluginRecovery.js +3 -2
  186. package/dist/serverPluginRecovery.js.map +1 -1
  187. package/dist/shared/apiTypes.js.map +1 -1
  188. package/dist/shared/i18n.js +41 -0
  189. package/dist/shared/i18n.js.map +1 -0
  190. package/dist/shared/piWebConfigParsing.js +52 -49
  191. package/dist/shared/piWebConfigParsing.js.map +1 -1
  192. package/dist/shared/pluginApiTypes.d.ts +7 -0
  193. package/dist/shared/requiredTerminalPlugin.js +1 -1
  194. package/dist/shared/requiredTerminalPlugin.js.map +1 -1
  195. package/dist/shared/sessionDefaults.js +7 -6
  196. package/dist/shared/sessionDefaults.js.map +1 -1
  197. package/dist/shared/thinkingLevels.js +17 -16
  198. package/dist/shared/thinkingLevels.js.map +1 -1
  199. package/docs/config.md +10 -1
  200. package/docs/plugins.md +8 -0
  201. package/package.json +1 -1
  202. package/dist/client/assets/index-CFhaGb5m.js +0 -4357
@@ -1,12 +1,14 @@
1
+ import { UserFacingError, createTranslator, userFacingErrorMessage } from "../../shared/i18n.js";
1
2
  import { realpathSync, statSync } from "node:fs";
2
3
  import { clientSessionFirstMessagePreview } from "./clientSessionPreview.js";
3
4
  import { readFile } from "node:fs/promises";
4
5
  import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
5
- import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEventBus, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, getAgentDir, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, ProjectTrustStore, hasTrustRequiringProjectResources, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
6
+ import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEventBus, createEditToolDefinition, createFindToolDefinition, createGrepToolDefinition, getAgentDir, createLsToolDefinition, createReadToolDefinition, createWriteToolDefinition, defineTool, loadSkills, ProjectTrustStore, hasTrustRequiringProjectResources, readStoredCredential, SessionManager, } from "@earendil-works/pi-coding-agent";
6
7
  import { projectBrowserMessage } from "../browserMessageProjection.js";
7
8
  import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
8
9
  import { eventScopeFromManagementContext, managementContextKey, NORMAL_SESSION_EVENT_SCOPE } from "../realtime/sessionEventScope.js";
9
- import { BUILTIN_COMMANDS } from "./builtinCommands.js";
10
+ import { builtinCommands } from "./builtinCommands.js";
11
+ import { loadNodeTranslator } from "../../localization.js";
10
12
  import { SessionCommandService } from "./sessionCommandService.js";
11
13
  import { SessionActivityMarker } from "./sessionActivityMarker.js";
12
14
  import { projectSessionTree } from "./sessionTreeProjection.js";
@@ -58,15 +60,12 @@ const DEFAULT_UNREAD_PUBLICATION_RETRY_MS = 1_000;
58
60
  * is inside: it awaits exactly one call for each, so the phase is a fact rather
59
61
  * than a guess. Deliberately free of internal symbol names and file paths.
60
62
  */
61
- const STARTUP_PHASE_RUNTIME = "正在启动 Pi 会话";
62
- const STARTUP_PHASE_EXTENSIONS = "正在加载会话扩展";
63
63
  /**
64
64
  * Appended to whichever phase is running when a background provider catalog
65
65
  * refresh happens to be in flight. It is stated as a concurrent fact, never as
66
66
  * the cause: PI WEB can verify that a refresh is running, but not that this
67
67
  * particular startup is waiting on it.
68
68
  */
69
- const STARTUP_CONCURRENT_CATALOG_REFRESH = "服务商模型列表正在刷新";
70
69
  const MAX_UNREAD_PUBLICATION_RETRY_MS = 30_000;
71
70
  const MAX_PENDING_UNREAD_MUTATIONS = SESSION_UNREAD_LIMIT + 1;
72
71
  const IDLE_SESSION_FILE_RESOLUTION_THROTTLE_MS = 30_000;
@@ -84,13 +83,13 @@ function auditVisibleAssistantMessage(message) {
84
83
  function noop() {
85
84
  // Intentionally empty default unsubscribe callback.
86
85
  }
87
- function spawnTargetError(decision) {
86
+ function spawnTargetError(decision, t) {
88
87
  if (decision.reason === "not-registered")
89
- return new Error("派生会话不在已注册项目中");
90
- return new Error(`cwd 必须是此项目的工作区。允许的路径:${decision.allowedCwds.join(", ")}`);
88
+ return new Error(t("session.spawnUnregistered", "Spawned session is outside registered projects"));
89
+ return new Error(t("session.spawnWorkspace", "cwd must be a workspace of this project. Allowed paths: {paths}", { paths: decision.allowedCwds.join(", ") }));
91
90
  }
92
- function subsessionCwdError(spawningCwd, requestedCwd) {
93
- return new Error(`跟踪子会话只能在父会话的工作区中运行(${spawningCwd}),不能使用 ${requestedCwd}。如需在其它工作区运行,请使用 spawn_session 创建独立会话。`);
91
+ function subsessionCwdError(spawningCwd, requestedCwd, t = loadNodeTranslator("server")) {
92
+ return new Error(t("server.main.3122d89b6904", "A tracked subsession runs in this session's working directory ({arg0}); {arg1} was requested. Instruct the child to work elsewhere from this workspace, or use spawn_session for an independent session in another workspace.", { arg0: spawningCwd, arg1: requestedCwd }));
94
93
  }
95
94
  function modelSpecOf(model) {
96
95
  return `${model.provider}/${model.id}`;
@@ -111,8 +110,8 @@ function parseModelSpec(spec) {
111
110
  * (a list would invite guesses). The agent loop turns the throw into an
112
111
  * error tool result; how to recover is the agent's call.
113
112
  */
114
- function unknownSpawnModelError(modelSpec) {
115
- return new Error(`未找到模型“${modelSpec}”。请传入精确的“provider/model-id”。`);
113
+ function unknownSpawnModelError(modelSpec, t) {
114
+ return new Error(t("model.spawnUnknown", 'Model "{model}" not found. Supply an exact "provider/model-id".', { model: modelSpec }));
116
115
  }
117
116
  function authLossWarningKey(sessionId, eventScope, provider, modelId) {
118
117
  return `${eventScope}:${sessionId}:${provider}/${modelId}`;
@@ -132,7 +131,7 @@ function lookupMatchesStartupSession(ref, session) {
132
131
  function activeSessionKey(sessionId, eventScope) {
133
132
  return `${eventScope}\0${sessionId}`;
134
133
  }
135
- function sessionTreeNavigationOptions(request) {
134
+ function sessionTreeNavigationOptions(request, t = loadNodeTranslator("server")) {
136
135
  switch (request.summary.mode) {
137
136
  case "none":
138
137
  return { summarize: false };
@@ -141,9 +140,9 @@ function sessionTreeNavigationOptions(request) {
141
140
  case "custom": {
142
141
  const customInstructions = request.summary.instructions.trim();
143
142
  if (customInstructions === "")
144
- throw new Error("自定义分支汇总要求不能为空");
143
+ throw new Error(t("server.main.416db415bedb", "Custom branch-summary instructions are required"));
145
144
  if (customInstructions.length > SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH) {
146
- throw new Error(`自定义分支汇总要求不能超过 ${String(SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH)} 个字符`);
145
+ throw new Error(t("server.main.8e5676bb8f61", "Custom branch-summary instructions must be at most {arg0} characters", { arg0: String(SESSION_TREE_CUSTOM_INSTRUCTIONS_MAX_LENGTH) }));
147
146
  }
148
147
  return { summarize: true, customInstructions };
149
148
  }
@@ -163,9 +162,9 @@ function decrementMapCount(counts, key) {
163
162
  else
164
163
  counts.set(key, remaining);
165
164
  }
166
- function requirePromptText(value) {
165
+ function requirePromptText(value, t) {
167
166
  if (typeof value !== "string")
168
- throw new Error("提示文本为必填项");
167
+ throw new Error(t("session.promptRequired", "Prompt text is required"));
169
168
  return value;
170
169
  }
171
170
  function parsePromptStreamingBehavior(value) {
@@ -173,7 +172,7 @@ function parsePromptStreamingBehavior(value) {
173
172
  return undefined;
174
173
  if (value === "steer" || value === "followUp")
175
174
  return value;
176
- throw new Error('Prompt streamingBehavior must be "steer" or "followUp"');
175
+ throw new UserFacingError("routing.streamingBehaviorInvalid", "Prompt streamingBehavior must be \"steer\" or \"followUp\"");
177
176
  }
178
177
  const GLOBAL_AGENT_CONTEXT_FILE_NAMES = new Set(["AGENTS.md", "AGENTS.MD", "CLAUDE.md", "CLAUDE.MD"]);
179
178
  export function filterPrivilegedManagementContextFiles(cwd, context, base) {
@@ -199,30 +198,46 @@ export function filterManagedGlobalContextFiles(cwd, agentDir, base) {
199
198
  }),
200
199
  };
201
200
  }
202
- export function filterManagedProjectSkills(cwd, base) {
201
+ export function filterManagedProjectSkills(cwd, base, agentDir = getAgentDir()) {
203
202
  const projectRoot = realpathSync(cwd);
204
203
  const pathAllowed = (path) => pathInsideRealDirectory(projectRoot, path);
205
- return {
206
- ...base,
207
- skills: base.skills.filter((skill) => pathAllowed(skill.filePath)),
208
- diagnostics: filterManagedSkillDiagnostics(base.diagnostics, pathAllowed),
209
- };
204
+ return filterManagedSkills(cwd, agentDir, base, pathAllowed);
210
205
  }
211
- export function filterManagedWorkbenchSkills(cwd, base, receipt) {
206
+ export function filterManagedWorkbenchSkills(cwd, base, receipt, agentDir = getAgentDir()) {
212
207
  const projectRoot = realpathSync(cwd);
213
208
  const allowedDirectories = new Set([
214
209
  ...MANAGED_RELAY_SKILL_NAMES.map((name) => resolve(projectRoot, ".pi", "skills", name)),
215
210
  ...receipt.skills.map((skill) => resolve(projectRoot, ".pi", "skills", skill.directory)),
216
211
  ]);
217
212
  const pathAllowed = (path) => [...allowedDirectories].some((directory) => pathInsideRealDirectory(directory, path));
218
- return {
219
- ...base,
220
- skills: base.skills.filter((skill) => pathAllowed(skill.filePath)),
221
- diagnostics: filterManagedSkillDiagnostics(base.diagnostics, pathAllowed),
222
- };
213
+ return filterManagedSkills(cwd, agentDir, base, pathAllowed);
223
214
  }
224
- function filterManagedSkillDiagnostics(diagnostics, pathAllowed) {
225
- return diagnostics.filter((diagnostic) => diagnostic.path === undefined || pathAllowed(diagnostic.path));
215
+ function filterManagedSkills(cwd, agentDir, base, pathAllowed) {
216
+ const skills = base.skills.filter((skill) => pathAllowed(skill.filePath));
217
+ const shadowedPaths = new Set();
218
+ const diagnostics = base.diagnostics.filter((diagnostic) => {
219
+ const collision = diagnostic.collision;
220
+ if (diagnostic.type === "collision" && collision?.resourceType === "skill") {
221
+ const winnerAllowed = pathAllowed(collision.winnerPath);
222
+ const loserAllowed = pathAllowed(collision.loserPath);
223
+ if (!winnerAllowed && loserAllowed)
224
+ shadowedPaths.add(collision.loserPath);
225
+ return winnerAllowed && loserAllowed;
226
+ }
227
+ return diagnostic.path === undefined || pathAllowed(diagnostic.path);
228
+ });
229
+ // The SDK deduplicates names before this override. Restore permitted losers
230
+ // instead of letting a discarded global/package winner hide them entirely.
231
+ if (shadowedPaths.size > 0) {
232
+ const recovered = loadSkills({ cwd, agentDir, skillPaths: [...shadowedPaths], includeDefaults: false });
233
+ skills.push(...recovered.skills.filter((skill) => pathAllowed(skill.filePath)));
234
+ for (const diagnostic of recovered.diagnostics) {
235
+ if (!diagnostics.some((existing) => existing.type === diagnostic.type && existing.path === diagnostic.path && existing.message === diagnostic.message)) {
236
+ diagnostics.push(diagnostic);
237
+ }
238
+ }
239
+ }
240
+ return { ...base, skills, diagnostics };
226
241
  }
227
242
  function pathInsideRealDirectory(directory, path) {
228
243
  try {
@@ -337,13 +352,13 @@ export function anthropicSubscriptionWarning(session, authPath) {
337
352
  */
338
353
  export function dismissSessionWarning(session, dismissId) {
339
354
  if (dismissId !== ANTHROPIC_EXTRA_USAGE_DISMISS_ID) {
340
- throw new Error(`Unknown session warning dismiss id: ${dismissId}`);
355
+ throw new UserFacingError("routing.warningDismissInvalid", "Unknown session warning dismiss id: {arg0}", { arg0: dismissId });
341
356
  }
342
357
  session.settingsManager.setWarnings({ ...session.settingsManager.getWarnings(), anthropicExtraUsage: false });
343
358
  }
344
359
  function defaultCreateAgentRuntime(createRuntime, options) {
345
360
  if (!(options.sessionManager instanceof SessionManager))
346
- throw new Error("Default runtime creation requires an SDK SessionManager");
361
+ throw new UserFacingError("routing.runtimeManagerRequired", "Default runtime creation requires an SDK SessionManager");
347
362
  const runtimeFactory = createRuntimeWithContextAndOneShotSessionOptions(createRuntime, options.managementContext, options.initialModel, options.initialThinkingLevel, options.delegationToolsEnabled);
348
363
  return createAgentSessionRuntime(runtimeFactory, {
349
364
  cwd: options.cwd,
@@ -556,7 +571,7 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, sess
556
571
  settingsManager,
557
572
  resourceLoaderOptions: {
558
573
  agentsFilesOverride: (base) => filterManagedGlobalContextFiles(cwd, agentDir, base),
559
- skillsOverride: (base) => receipt === undefined ? filterManagedProjectSkills(cwd, base) : filterManagedWorkbenchSkills(cwd, base, receipt),
574
+ skillsOverride: (base) => receipt === undefined ? filterManagedProjectSkills(cwd, base, agentDir) : filterManagedWorkbenchSkills(cwd, base, receipt, agentDir),
560
575
  },
561
576
  }), projectTrustDiagnostics: [] };
562
577
  const modelOptions = await resolveSessionModelOptions({
@@ -585,7 +600,11 @@ function createManagementRuntimeFactory(modelRuntime, resolveSettingsScope, sess
585
600
  const result = await createAgentSessionFromServices(options);
586
601
  // SDK reload re-executes extensions, so it needs the same scoped environment as startup.
587
602
  const reload = result.session.reload.bind(result.session);
588
- result.session.reload = (options) => withRuntimeCreationEnvironment(resourceEnvironment, () => reload(options));
603
+ result.session.reload = (options) => withRuntimeCreationEnvironment(resourceEnvironment, async () => {
604
+ if (!normalPermissions)
605
+ await ensureManagedRelaySkills(cwd);
606
+ return reload(options);
607
+ });
589
608
  return { ...result, services, diagnostics: [...projectTrustDiagnostics, ...services.diagnostics] };
590
609
  });
591
610
  };
@@ -686,6 +705,7 @@ export class PiSessionService {
686
705
  this.pendingUnreadMutations = [];
687
706
  this.unreadPublicationFlushRequested = false;
688
707
  this.unreadPublicationStopped = false;
708
+ this.t = deps.translator ?? loadNodeTranslator("server");
689
709
  this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
690
710
  this.pinStore = deps.pinStore ?? new SessionPinStore(join(deps.dataDir ?? piWebDataDir(), "session-pins.json"));
691
711
  this.agentDir = deps.agentDir ?? getAgentDir();
@@ -741,13 +761,13 @@ export class PiSessionService {
741
761
  this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
742
762
  this.commandService = new SessionCommandService((sessionId, eventScope) => this.getActiveForCommand(sessionId, eventScope), (sessionId, text, eventScope) => this.submitCommandPrompt(sessionId, text, eventScope), events, {
743
763
  onCompactionStart: (session) => {
744
- this.beginSessionEntryMutation(session, "压缩会话");
745
- this.publishActivity(session, "正在压缩上下文", "active");
764
+ this.beginSessionEntryMutation(session, this.t("session.action.compact", "compact the session"));
765
+ this.publishActivity(session, this.t("activity.compacting", "Compacting context"), "active");
746
766
  this.publishStatus(session);
747
767
  },
748
768
  onCompactionEnd: (session, result, detail) => {
749
769
  this.endSessionEntryMutation(session);
750
- this.publishActivity(session, result === "success" ? "上下文压缩完成" : "上下文压缩失败", result === "success" ? "idle" : "error", detail);
770
+ this.publishActivity(session, result === "success" ? this.t("activity.compacted", "Context compacted") : this.t("activity.compactionFailed", "Context compaction failed"), result === "success" ? "idle" : "error", detail);
751
771
  this.publishStatus(session);
752
772
  },
753
773
  reloadSession: (session) => this.reloadSessionRuntime(session),
@@ -760,9 +780,9 @@ export class PiSessionService {
760
780
  isTreeNavigationActive: (session) => this.treeNavigations.has(session),
761
781
  runSessionReplacement: (session, operation) => {
762
782
  const runtime = this.activeRuntimeForSession(session);
763
- return this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session, ...(runtime === undefined ? {} : { runtime }) }], "替换会话前请先停止当前会话活动", operation);
783
+ return this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session, ...(runtime === undefined ? {} : { runtime }) }], this.t("session.stopBeforeReplace", "Stop current session activity before replacing the session"), operation);
764
784
  },
765
- }, { listSessionNames: (cwd) => this.listSessionNames(cwd) });
785
+ }, { listSessionNames: (cwd) => this.listSessionNames(cwd) }, this.t);
766
786
  }
767
787
  activeCount() {
768
788
  return this.active.size;
@@ -783,7 +803,7 @@ export class PiSessionService {
783
803
  const scope = eventScopeFromManagementContext(context);
784
804
  const plan = await this.cleanupPlan(request);
785
805
  if (plan.deleteRecords.length > 0 && this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
786
- throw new Error("Archive store does not support deletion");
806
+ throw new Error(this.t("session.archiveDeleteUnsupported", "The archive store does not support deletion"));
787
807
  const archiveInputs = [];
788
808
  const readyArchiveInputs = [];
789
809
  const deleteRecords = [];
@@ -900,13 +920,13 @@ export class PiSessionService {
900
920
  }
901
921
  async setPinned(ref, pinned, managementContext) {
902
922
  if (ref.cwd === "")
903
- throw new Error("置顶会话必须提供工作区");
923
+ throw new Error(this.t("session.pinWorkspaceRequired", "A workspace is required to pin a session"));
904
924
  const sessions = await this.list(ref.cwd, managementContext);
905
925
  const session = sessions.find((candidate) => candidate.id === ref.id);
906
926
  if (session === undefined)
907
- throw new Error("未找到会话");
927
+ throw new Error(this.t("session.notFound", "Session not found"));
908
928
  if (session.archived !== true && session.persisted === false)
909
- throw new Error("只能置顶已持久化会话");
929
+ throw new Error(this.t("session.pinPersistedOnly", "Only persisted sessions can be pinned"));
910
930
  await this.pinStore.set(sessionPinScope(ref.cwd, managementContext), session.id, pinned);
911
931
  return { pinned };
912
932
  }
@@ -950,7 +970,7 @@ export class PiSessionService {
950
970
  }
951
971
  /** Atomically start and prompt one visible normal-mode, non-delegating session. */
952
972
  async startOneShotRun(cwd, text, signal) {
953
- const promptText = requirePromptText(text);
973
+ const promptText = requirePromptText(text, this.t);
954
974
  return this.startManagedSession(cwd, signal, promptText);
955
975
  }
956
976
  async startManagedSession(cwd, signal, promptText) {
@@ -973,7 +993,7 @@ export class PiSessionService {
973
993
  await this.closeActive(session.sessionId);
974
994
  }
975
995
  catch (cleanupError) {
976
- throw new AggregateError([error, cleanupError], `托管会话 ${session.sessionId} 启动失败后清理失败`, { cause: cleanupError });
996
+ throw new AggregateError([error, cleanupError], this.t("server.main.dbc19a47e7b3", "Failed to clean up managed session {arg0} after startup failed", { arg0: session.sessionId }), { cause: cleanupError });
977
997
  }
978
998
  throw error;
979
999
  }
@@ -1014,10 +1034,10 @@ export class PiSessionService {
1014
1034
  */
1015
1035
  async spawnSession(input) {
1016
1036
  if (this.spawnTargets === undefined)
1017
- throw new Error("派生会话已禁用");
1037
+ throw new Error(this.t("session.spawningDisabled", "Session spawning is disabled"));
1018
1038
  const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, input.cwd, eventScopeFromManagementContext(input.managementContext));
1019
1039
  if (!decision.allowed)
1020
- throw spawnTargetError(decision);
1040
+ throw spawnTargetError(decision, this.t);
1021
1041
  // A model spec overrides the inherited model. Only a spec triggers a
1022
1042
  // spawning-session lookup; the default path must not depend on it.
1023
1043
  let model = input.model;
@@ -1043,13 +1063,13 @@ export class PiSessionService {
1043
1063
  /** Start a tracked child in the parent's workspace so it remains visible in the same session tree. */
1044
1064
  async spawnSubsession(input) {
1045
1065
  if (this.spawnTargets === undefined)
1046
- throw new Error("派生会话已禁用");
1066
+ throw new Error(this.t("session.spawningDisabled", "Session spawning is disabled"));
1047
1067
  if (input.cwd !== undefined && input.cwd !== "" && !cwdPathsEqual(input.cwd, input.spawningCwd)) {
1048
- throw subsessionCwdError(input.spawningCwd, input.cwd);
1068
+ throw subsessionCwdError(input.spawningCwd, input.cwd, this.t);
1049
1069
  }
1050
1070
  const decision = await this.spawnTargets.resolveSpawnTarget(input.spawningCwd, undefined, eventScopeFromManagementContext(input.managementContext));
1051
1071
  if (!decision.allowed)
1052
- throw spawnTargetError(decision);
1072
+ throw spawnTargetError(decision, this.t);
1053
1073
  // A model spec overrides the inherited model and is resolved against the
1054
1074
  // parent's model runtime; only a spec triggers that lookup.
1055
1075
  const model = input.modelSpec === undefined
@@ -1196,7 +1216,7 @@ export class PiSessionService {
1196
1216
  : candidates.find((candidate) => candidate.provider === parsed.provider && candidate.id === parsed.modelId)
1197
1217
  ?? session.modelRuntime.getModel(parsed.provider, parsed.modelId);
1198
1218
  if (model === undefined)
1199
- throw unknownSpawnModelError(modelSpec);
1219
+ throw unknownSpawnModelError(modelSpec, this.t);
1200
1220
  return model;
1201
1221
  }
1202
1222
  /**
@@ -1233,7 +1253,7 @@ export class PiSessionService {
1233
1253
  const session = await this.getOrOpen(ref, managementContext);
1234
1254
  // Checked before the store closes the ask so a refused delivery cannot
1235
1255
  // discard answers the user already submitted.
1236
- this.assertTreeNavigationInactive(session, "回答问题");
1256
+ this.assertTreeNavigationInactive(session, this.t("session.action.answer", "answer questions"));
1237
1257
  return this.closeAsk(session, this.pendingAskStore.submit(this.dialogSessionKey(session), askId, submission));
1238
1258
  }
1239
1259
  /**
@@ -1244,7 +1264,7 @@ export class PiSessionService {
1244
1264
  async cancelAsk(ref, askId, managementContext) {
1245
1265
  await this.assertWritable(ref);
1246
1266
  const session = await this.getOrOpen(ref, managementContext);
1247
- this.assertTreeNavigationInactive(session, "关闭问题");
1267
+ this.assertTreeNavigationInactive(session, this.t("session.action.dismiss", "dismiss questions"));
1248
1268
  return this.closeAsk(session, this.pendingAskStore.cancel(this.dialogSessionKey(session), askId));
1249
1269
  }
1250
1270
  /**
@@ -1258,7 +1278,7 @@ export class PiSessionService {
1258
1278
  return { result: "stale", sessionStatus: this.statusFromSession(session) };
1259
1279
  const { outcome } = result;
1260
1280
  this.publishAskClosed(session.sessionId, outcome, this.eventScopeForSession(session));
1261
- await this.runSessionEntryMutation(session, "提交问题回答", () => session.sendCustomMessage({ customType: ASK_USER_ANSWERS_CUSTOM_TYPE, content: renderAskUserAnswersText(outcome), display: true, details: outcome }, { triggerTurn: true, deliverAs: "followUp" }));
1281
+ await this.runSessionEntryMutation(session, this.t("server.main.07f784061918", "deliver answers to your questions"), () => session.sendCustomMessage({ customType: ASK_USER_ANSWERS_CUSTOM_TYPE, content: renderAskUserAnswersText(outcome), display: true, details: outcome }, { triggerTurn: true, deliverAs: "followUp" }));
1262
1282
  this.publishStatus(session);
1263
1283
  return { result: "closed", outcome, sessionStatus: this.statusFromSession(session) };
1264
1284
  }
@@ -1276,7 +1296,7 @@ export class PiSessionService {
1276
1296
  if (outcome === undefined)
1277
1297
  return;
1278
1298
  this.publishAskClosed(session.sessionId, outcome, this.eventScopeForSession(session));
1279
- await this.runSessionEntryMutation(session, "作废未回答问题", () => session.sendCustomMessage({ customType: ASK_USER_ANSWERS_CUSTOM_TYPE, content: renderAskUserAnswersText(outcome), display: true, details: outcome }, { triggerTurn: false, deliverAs: "followUp" }));
1299
+ await this.runSessionEntryMutation(session, this.t("server.main.90b5a8ac7c7d", "void the open questions"), () => session.sendCustomMessage({ customType: ASK_USER_ANSWERS_CUSTOM_TYPE, content: renderAskUserAnswersText(outcome), display: true, details: outcome }, { triggerTurn: false, deliverAs: "followUp" }));
1280
1300
  this.publishStatus(session);
1281
1301
  }
1282
1302
  /**
@@ -1509,10 +1529,10 @@ export class PiSessionService {
1509
1529
  return canonicalizeStoredCwd(this.sessionManager.open(childSessionFile).getCwd());
1510
1530
  }
1511
1531
  catch (error) {
1512
- throw new Error("Could not resolve cwd for verified tracked sub-session", { cause: error });
1532
+ throw new Error(this.t("session.subsessionCwdMissing", "Could not resolve cwd for verified tracked sub-session"), { cause: error });
1513
1533
  }
1514
1534
  }
1515
- throw new Error("Could not resolve cwd for verified tracked sub-session");
1535
+ throw new Error(this.t("session.subsessionCwdMissing", "Could not resolve cwd for verified tracked sub-session"));
1516
1536
  }
1517
1537
  unregisterSubsession(childSessionId, scope) {
1518
1538
  const childKey = activeSessionKey(childSessionId, scope);
@@ -1638,13 +1658,13 @@ export class PiSessionService {
1638
1658
  return active.runtime.session;
1639
1659
  if (link.childSessionFile !== undefined) {
1640
1660
  if (!(await sessionFileHeaderMatches(link.childSessionFile, { sessionId, parentSessionFile: link.parentSessionFile })))
1641
- throw new Error("Session not found");
1661
+ throw new Error(this.t("session.notFound", "Session not found"));
1642
1662
  const sessionManager = this.sessionManager.open(link.childSessionFile);
1643
1663
  return (await this.create(sessionManager, link.cwd ?? sessionManager.getCwd(), {
1644
1664
  ...(link.managementContext === undefined ? {} : { managementContext: link.managementContext }),
1645
1665
  })).runtime.session;
1646
1666
  }
1647
- throw new Error("Session not found");
1667
+ throw new Error(this.t("session.notFound", "Session not found"));
1648
1668
  }
1649
1669
  async subsessionSummaryFields(childSessionId, scope) {
1650
1670
  const link = this.subsessionLinks.get(activeSessionKey(childSessionId, scope));
@@ -1712,9 +1732,9 @@ export class PiSessionService {
1712
1732
  return active.runtime.session;
1713
1733
  const parentSessionFile = link.parentSessionFile;
1714
1734
  if (parentSessionFile === undefined)
1715
- throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
1735
+ throw new Error(this.t("session.parentUnavailable", "Parent session {parentSessionId} is not available for subsession notification", { parentSessionId }));
1716
1736
  if ((await readSessionHeaderSummary(parentSessionFile))?.id !== parentSessionId) {
1717
- throw new Error(`Parent session ${parentSessionId} is not available for subsession notification`);
1737
+ throw new Error(this.t("session.parentUnavailable", "Parent session {parentSessionId} is not available for subsession notification", { parentSessionId }));
1718
1738
  }
1719
1739
  const sessionManager = this.sessionManager.open(parentSessionFile);
1720
1740
  return (await this.create(sessionManager, sessionManager.getCwd(), {
@@ -1746,7 +1766,8 @@ export class PiSessionService {
1746
1766
  }
1747
1767
  async messages(ref, page, managementContext) {
1748
1768
  const session = await this.getOrOpen(ref, managementContext);
1749
- return pageMessagesAtSafeBoundary(historyMessagesFromEntries(await this.readableSessionBranch(session)), page);
1769
+ const branch = await this.readableSessionBranch(session);
1770
+ return { ...pageMessagesAtSafeBoundary(historyMessagesFromEntries(branch), page), leafId: session.sessionManager.getLeafId() };
1750
1771
  }
1751
1772
  async status(ref, managementContext) {
1752
1773
  const session = await this.sessionForStatusOrDialogClose(ref, managementContext);
@@ -1803,7 +1824,7 @@ export class PiSessionService {
1803
1824
  const target = `${update.provider}/${update.modelId}`;
1804
1825
  const available = session.modelRuntime.getAvailableSnapshot();
1805
1826
  if (!available.some((model) => modelScopeId(model) === target))
1806
- throw new Error(`未找到模型:${target}`);
1827
+ throw new Error(this.t("server.main.583c6c767d31", "Model not found: {arg0}", { arg0: target }));
1807
1828
  const enabledIds = await resolveEnabledModelIds({
1808
1829
  settingsManager: settings,
1809
1830
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -1811,7 +1832,7 @@ export class PiSessionService {
1811
1832
  scopedModels: [],
1812
1833
  });
1813
1834
  if (enabledIds !== null && !enabledIds.includes(target))
1814
- throw new Error(`模型未启用:${target}`);
1835
+ throw new Error(this.t("server.main.c24071bdc942", "Model is not enabled: {arg0}", { arg0: target }));
1815
1836
  settings.setDefaultModelAndProvider(update.provider, update.modelId);
1816
1837
  }
1817
1838
  else if (update.thinkingLevel !== undefined) {
@@ -1839,7 +1860,7 @@ export class PiSessionService {
1839
1860
  assertDefaultsSettingsHealthy(settings) {
1840
1861
  const errors = settings.drainErrors();
1841
1862
  if (errors.length > 0)
1842
- throw new Error(`会话默认值设置失败:${errors.map(({ error }) => error.message).join("; ")}`);
1863
+ throw new Error(this.t("server.main.d04dfe5abc11", "Session defaults settings failed: {arg0}", { arg0: errors.map(({ error }) => error.message).join("; ") }));
1843
1864
  }
1844
1865
  async availableModels(ref, managementContext) {
1845
1866
  const session = await this.getOrOpen(ref, managementContext);
@@ -1853,25 +1874,25 @@ export class PiSessionService {
1853
1874
  }
1854
1875
  async setModelEnabled(ref, provider, modelId, enabled, managementContext) {
1855
1876
  if (managementContext !== undefined)
1856
- throw new Error("管理嵌入模式不允许修改已启用模型范围");
1877
+ throw new Error(this.t("server.main.2a7cb0e35521", "Enabled model scope cannot be changed in management embed mode"));
1857
1878
  await this.assertWritable(ref);
1858
1879
  const session = await this.getOrOpen(ref);
1859
1880
  return this.runModelScopeMutation(async () => {
1860
1881
  const scope = this.modelScopeContext(session);
1861
1882
  if (!scope.editable)
1862
- throw new Error("模型可用范围由此工作区的 .pi/settings.json 控制");
1863
- this.assertTreeNavigationInactive(session, "修改已启用模型");
1883
+ throw new Error(this.t("server.main.c43b4e3cefe1", "Model availability is controlled by this workspace's .pi/settings.json"));
1884
+ this.assertTreeNavigationInactive(session, this.t("server.main.fd81137332d5", "change enabled models"));
1864
1885
  await session.modelRuntime.refresh({ allowNetwork: false });
1865
1886
  const currentIds = (await this.modelScopeSnapshot(session, scope)).enabledIds;
1866
1887
  const available = session.modelRuntime.getAvailableSnapshot();
1867
1888
  const availableIds = available.map(modelScopeId);
1868
1889
  const targetId = `${provider}/${modelId}`;
1869
1890
  if (!availableIds.includes(targetId))
1870
- throw new Error(`未找到模型:${targetId}`);
1891
+ throw new Error(this.t("server.main.583c6c767d31", "Model not found: {arg0}", { arg0: targetId }));
1871
1892
  if (!enabled && session.model !== undefined && modelScopeId(session.model) === targetId)
1872
- throw new Error("不能停用当前模型");
1893
+ throw new Error(this.t("server.main.fe143303e69e", "Current model cannot be disabled"));
1873
1894
  const nextIds = applyEnabledModelToggle(currentIds, availableIds, targetId, enabled);
1874
- this.assertTreeNavigationInactive(session, "修改已启用模型");
1895
+ this.assertTreeNavigationInactive(session, this.t("server.main.fd81137332d5", "change enabled models"));
1875
1896
  if (nextIds !== currentIds)
1876
1897
  this.applyEnabledModelScope(session, available, nextIds);
1877
1898
  return (await this.enabledModelCatalog(session)).map((entry) => catalogEntryToClientModel(entry));
@@ -1879,14 +1900,14 @@ export class PiSessionService {
1879
1900
  }
1880
1901
  async setModelScope(ref, mode, managementContext) {
1881
1902
  if (managementContext !== undefined)
1882
- throw new Error("管理嵌入模式不允许修改已启用模型范围");
1903
+ throw new Error(this.t("server.main.2a7cb0e35521", "Enabled model scope cannot be changed in management embed mode"));
1883
1904
  await this.assertWritable(ref);
1884
1905
  const session = await this.getOrOpen(ref);
1885
1906
  return this.runModelScopeMutation(async () => {
1886
1907
  const scope = this.modelScopeContext(session);
1887
1908
  if (!scope.editable)
1888
- throw new Error("模型可用范围由此工作区的 .pi/settings.json 控制");
1889
- this.assertTreeNavigationInactive(session, "修改已启用模型");
1909
+ throw new Error(this.t("server.main.c43b4e3cefe1", "Model availability is controlled by this workspace's .pi/settings.json"));
1910
+ this.assertTreeNavigationInactive(session, this.t("server.main.fd81137332d5", "change enabled models"));
1890
1911
  await session.modelRuntime.refresh({ allowNetwork: false });
1891
1912
  const available = session.modelRuntime.getAvailableSnapshot();
1892
1913
  const availableIds = available.map(modelScopeId);
@@ -1894,10 +1915,10 @@ export class PiSessionService {
1894
1915
  if (mode === "current") {
1895
1916
  const current = session.model;
1896
1917
  if (current === undefined || !availableIds.includes(modelScopeId(current)))
1897
- throw new Error("当前模型不可用");
1918
+ throw new Error(this.t("server.main.46b71cd9c032", "The current model is unavailable"));
1898
1919
  nextIds = [modelScopeId(current)];
1899
1920
  }
1900
- this.assertTreeNavigationInactive(session, "修改已启用模型");
1921
+ this.assertTreeNavigationInactive(session, this.t("server.main.fd81137332d5", "change enabled models"));
1901
1922
  this.applyEnabledModelScope(session, available, nextIds);
1902
1923
  return (await this.enabledModelCatalog(session)).map((entry) => catalogEntryToClientModel(entry));
1903
1924
  });
@@ -1905,15 +1926,15 @@ export class PiSessionService {
1905
1926
  async setModel(ref, provider, modelId, managementContext) {
1906
1927
  await this.assertWritable(ref);
1907
1928
  const session = await this.getOrOpen(ref, managementContext);
1908
- this.assertTreeNavigationInactive(session, "更改模型");
1929
+ this.assertTreeNavigationInactive(session, this.t("server.main.130226a572df", "change models"));
1909
1930
  const candidates = await this.sessionModelCandidates(session);
1910
- this.assertTreeNavigationInactive(session, "更改模型");
1931
+ this.assertTreeNavigationInactive(session, this.t("server.main.130226a572df", "change models"));
1911
1932
  const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
1912
1933
  ?? session.modelRuntime.getModel(provider, modelId);
1913
1934
  if (model === undefined)
1914
- throw new Error(`未找到模型:${provider}/${modelId}`);
1915
- await this.runSessionEntryMutation(session, "更改模型", () => session.setModel(model));
1916
- this.publishActivity(session, `模型:${model.id}`, "idle", model.provider);
1935
+ throw new Error(this.t("model.notFound", "Model not found: {value0}/{value1}", { value0: provider, value1: modelId }));
1936
+ await this.runSessionEntryMutation(session, this.t("server.main.130226a572df", "change models"), () => session.setModel(model));
1937
+ this.publishActivity(session, this.t("activity.model", "Model: {value0}", { value0: model.id }), "idle", model.provider);
1917
1938
  this.publishStatus(session);
1918
1939
  return this.statusFromSession(session);
1919
1940
  }
@@ -1923,11 +1944,11 @@ export class PiSessionService {
1923
1944
  const result = await this.runModelScopeMutation(async () => {
1924
1945
  await session.modelRuntime.refresh({ allowNetwork: false });
1925
1946
  await this.synchronizeSessionModelScope(session, session.modelRuntime.getAvailableSnapshot());
1926
- return this.runSessionEntryMutation(session, "更改模型", () => session.cycleModel(direction));
1947
+ return this.runSessionEntryMutation(session, this.t("server.main.130226a572df", "change models"), () => session.cycleModel(direction));
1927
1948
  });
1928
1949
  if (result === undefined)
1929
- throw new Error(session.scopedModels.length > 0 ? "作用域内只有一个模型" : "只有一个可用模型");
1930
- this.publishActivity(session, `模型:${result.model.id}`, "idle", result.model.provider);
1950
+ throw new Error(session.scopedModels.length > 0 ? this.t("model.oneScoped", "Only one scoped model is available") : this.t("model.oneAvailable", "Only one model is available"));
1951
+ this.publishActivity(session, this.t("activity.model", "Model: {value0}", { value0: result.model.id }), "idle", result.model.provider);
1931
1952
  this.publishStatus(session);
1932
1953
  return this.statusFromSession(session);
1933
1954
  }
@@ -1938,34 +1959,34 @@ export class PiSessionService {
1938
1959
  async setThinkingLevel(ref, level, managementContext) {
1939
1960
  await this.assertWritable(ref);
1940
1961
  const session = await this.getOrOpen(ref, managementContext);
1941
- this.assertTreeNavigationInactive(session, "更改思考级别");
1962
+ this.assertTreeNavigationInactive(session, this.t("session.action.changeThinking", "change the thinking level"));
1942
1963
  // pi owns the valid set; validate against the session's live levels rather
1943
1964
  // than a hardcoded union so this stays correct if pi changes the set.
1944
1965
  const available = session.getAvailableThinkingLevels();
1945
1966
  const match = available.find((candidate) => candidate === level);
1946
1967
  if (match === undefined)
1947
- throw new Error(`无效的思考级别:${level}`);
1948
- await this.runSessionEntryMutation(session, "更改思考级别", () => {
1968
+ throw new Error(this.t("model.invalidThinking", "Invalid thinking level: {value0}", { value0: level }));
1969
+ await this.runSessionEntryMutation(session, this.t("session.action.changeThinking", "change the thinking level"), () => {
1949
1970
  session.setThinkingLevel(match);
1950
1971
  return Promise.resolve();
1951
1972
  });
1952
- this.publishActivity(session, `思考级别:${session.thinkingLevel}`, "idle");
1973
+ this.publishActivity(session, this.t("activity.thinking", "Thinking level: {value0}", { value0: session.thinkingLevel }), "idle");
1953
1974
  this.publishStatus(session);
1954
1975
  return this.statusFromSession(session);
1955
1976
  }
1956
1977
  async cycleThinkingLevel(ref, managementContext) {
1957
1978
  await this.assertWritable(ref);
1958
1979
  const session = await this.getOrOpen(ref, managementContext);
1959
- const level = await this.runSessionEntryMutation(session, "更改思考级别", () => Promise.resolve(session.cycleThinkingLevel()));
1980
+ const level = await this.runSessionEntryMutation(session, this.t("session.action.changeThinking", "change the thinking level"), () => Promise.resolve(session.cycleThinkingLevel()));
1960
1981
  if (level === undefined)
1961
- throw new Error("当前模型不支持思考");
1962
- this.publishActivity(session, `思考级别:${level}`, "idle");
1982
+ throw new Error(this.t("model.thinkingUnsupported", "The current model does not support thinking"));
1983
+ this.publishActivity(session, this.t("ui.promptEditor.thinkingLevel", "Thinking level: {thinkingLevel}", { thinkingLevel: level }), "idle");
1963
1984
  this.publishStatus(session);
1964
1985
  return this.statusFromSession(session);
1965
1986
  }
1966
1987
  async commands(ref, managementContext) {
1967
1988
  const session = await this.getOrOpen(ref, managementContext);
1968
- const commands = [...BUILTIN_COMMANDS];
1989
+ const commands = builtinCommands(this.t);
1969
1990
  for (const command of session.extensionRunner.getRegisteredCommands()) {
1970
1991
  commands.push({ name: command.invocationName, ...(command.description === undefined ? {} : { description: command.description }), source: "extension" });
1971
1992
  }
@@ -1983,7 +2004,7 @@ export class PiSessionService {
1983
2004
  return commands.sort((a, b) => a.name.localeCompare(b.name));
1984
2005
  }
1985
2006
  async prompt(ref, text, streamingBehavior, attachments, options) {
1986
- const promptText = requirePromptText(text);
2007
+ const promptText = requirePromptText(text, this.t);
1987
2008
  // Command-forwarded prompts (e.g. /skill:*) are expanded by the agent, which
1988
2009
  // streams the canonical message back. The client doesn't render the raw
1989
2010
  // command text, so the server must not echo it either, or it would show up
@@ -1994,12 +2015,12 @@ export class PiSessionService {
1994
2015
  const images = (await attachmentsToInlineImages(parsedAttachments)).map((entry) => entry.image);
1995
2016
  await this.assertWritable(ref);
1996
2017
  const session = await this.getOrOpen(ref, options?.managementContext);
1997
- this.assertTreeNavigationInactive(session, "发送消息");
2018
+ this.assertTreeNavigationInactive(session, this.t("server.main.94306b2fc35c", "send a prompt"));
1998
2019
  this.maybeGenerateSessionName(session, promptText);
1999
2020
  const isQueued = session.isStreaming || session.isCompacting;
2000
2021
  const behavior = isQueued ? requestedBehavior ?? "followUp" : undefined;
2001
2022
  if (isQueued && images.length === 0 && this.hasQueuedMessageText(session, promptText)) {
2002
- this.publishActivity(session, "已忽略重复的排队消息", "active");
2023
+ this.publishActivity(session, this.t("activity.duplicateIgnored", "Duplicate queued message ignored"), "active");
2003
2024
  this.publishStatus(session);
2004
2025
  return;
2005
2026
  }
@@ -2021,15 +2042,15 @@ export class PiSessionService {
2021
2042
  }
2022
2043
  beginPromptSubmission(session, text, behavior, images = [], echoUserMessage = true) {
2023
2044
  this.auditUserPrompt(session, text);
2024
- this.publishActivity(session, behavior === "steer" ? "插队消息已排队" : behavior === "followUp" ? "消息已排队" : "消息已接收", "active");
2045
+ this.publishActivity(session, behavior === "steer" ? this.t("activity.steerQueued", "Steering message queued") : behavior === "followUp" ? this.t("activity.messageQueued", "Message queued") : this.t("activity.messageReceived", "Message received"), "active");
2025
2046
  const eventScope = this.eventScopeForSession(session);
2026
2047
  if (behavior === undefined && echoUserMessage)
2027
2048
  this.events.publish(session.sessionId, { type: "message.append", message: userMessage(text, images) }, eventScope);
2028
2049
  const promptOptions = buildPromptOptions(behavior, images);
2029
- const promptPromise = this.runSessionEntryMutation(session, "发送消息", () => session.prompt(text, promptOptions));
2050
+ const promptPromise = this.runSessionEntryMutation(session, this.t("server.main.94306b2fc35c", "send a prompt"), () => session.prompt(text, promptOptions));
2030
2051
  void promptPromise.catch((error) => {
2031
2052
  const message = error instanceof Error ? error.message : String(error);
2032
- this.publishActivity(session, "发生错误", "error", message);
2053
+ this.publishActivity(session, this.t("activity.error", "An error occurred"), "error", message);
2033
2054
  this.events.publish(session.sessionId, { type: "session.error", message }, eventScope);
2034
2055
  });
2035
2056
  return promptPromise;
@@ -2048,7 +2069,7 @@ export class PiSessionService {
2048
2069
  }
2049
2070
  if (event["type"] === "auto_retry_end" && event["success"] === false && event["finalError"] === "Retry cancelled") {
2050
2071
  // Native Pi cancels retry sleep without an aborted assistant message.
2051
- failure = new DOMException("初始 PI 会话运行已中止", "AbortError");
2072
+ failure = new DOMException(this.t("server.main.ed6dfd6b55ab", "Initial PI session run was aborted"), "AbortError");
2052
2073
  return;
2053
2074
  }
2054
2075
  const message = event["message"];
@@ -2068,9 +2089,9 @@ export class PiSessionService {
2068
2089
  // prompt promise can fulfill even when the final assistant is an error.
2069
2090
  const reason = message["stopReason"];
2070
2091
  failure = reason === "aborted"
2071
- ? new DOMException("初始 PI 会话运行已中止", "AbortError")
2092
+ ? new DOMException(this.t("server.main.ed6dfd6b55ab", "Initial PI session run was aborted"), "AbortError")
2072
2093
  : reason === "error"
2073
- ? new Error(typeof message["errorMessage"] === "string" ? message["errorMessage"] : "初始 PI 会话运行失败")
2094
+ ? new Error(typeof message["errorMessage"] === "string" ? message["errorMessage"] : this.t("server.main.e08c1380a71f", "Initial PI session run failed"))
2074
2095
  : undefined;
2075
2096
  });
2076
2097
  try {
@@ -2087,7 +2108,7 @@ export class PiSessionService {
2087
2108
  const queue = this.compactionPromptQueues.get(sessionKey) ?? [];
2088
2109
  queue.push({ kind, text, ...(images.length > 0 ? { images } : {}), ...(echoUserMessage ? {} : { echoUserMessage: false }) });
2089
2110
  this.compactionPromptQueues.set(sessionKey, queue);
2090
- this.publishActivity(session, "压缩期间消息已排队", "active");
2111
+ this.publishActivity(session, this.t("activity.compactionQueued", "Message queued during compaction"), "active");
2091
2112
  this.publishStatus(session);
2092
2113
  }
2093
2114
  async saveAttachments(ref, attachments, folder, managementContext) {
@@ -2108,19 +2129,19 @@ export class PiSessionService {
2108
2129
  await this.assertWritable(ref);
2109
2130
  const active = await this.getActive(ref, managementContext);
2110
2131
  const { session } = active.runtime;
2111
- this.assertTreeNavigationInactive(session, "运行 Shell 命令");
2132
+ this.assertTreeNavigationInactive(session, this.t("session.action.shell", "run a shell command"));
2112
2133
  const isExcluded = text.startsWith("!!");
2113
2134
  const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
2114
2135
  if (!command)
2115
- throw new Error("用法:!<shell command>");
2136
+ throw new Error(this.t("shell.usage", "Usage: !<shell command>"));
2116
2137
  if (session.isBashRunning)
2117
- throw new Error("已有 bash 命令正在运行");
2138
+ throw new Error(this.t("shell.alreadyRunning", "A bash command is already running"));
2118
2139
  const eventScope = active.eventScope;
2119
- this.publishActivity(session, "正在运行命令", "active", command);
2140
+ this.publishActivity(session, this.t("activity.commandRunning", "Running command"), "active", command);
2120
2141
  this.events.publish(session.sessionId, { type: "shell.start", command, excludeFromContext: isExcluded }, eventScope);
2121
- void this.runSessionEntryMutation(session, "运行 Shell 命令", () => session.executeBash(command, (chunk) => {
2142
+ void this.runSessionEntryMutation(session, this.t("session.action.shell", "run a shell command"), () => session.executeBash(command, (chunk) => {
2122
2143
  this.events.publish(session.sessionId, { type: "shell.chunk", chunk }, eventScope);
2123
- this.publishActivity(session, "正在运行命令", "active", command);
2144
+ this.publishActivity(session, this.t("activity.commandRunning", "Running command"), "active", command);
2124
2145
  this.publishStatus(session);
2125
2146
  }, { excludeFromContext: isExcluded })).then((result) => {
2126
2147
  this.events.publish(session.sessionId, {
@@ -2131,13 +2152,13 @@ export class PiSessionService {
2131
2152
  truncated: result.truncated,
2132
2153
  ...(result.fullOutputPath === undefined ? {} : { fullOutputPath: result.fullOutputPath }),
2133
2154
  }, eventScope);
2134
- this.publishActivity(session, "命令执行完成", result.exitCode === 0 ? "idle" : "error", command);
2155
+ this.publishActivity(session, this.t("activity.commandCompleted", "Command completed"), result.exitCode === 0 ? "idle" : "error", command);
2135
2156
  this.publishStatus(session);
2136
2157
  }).catch((error) => {
2137
- const message = error instanceof Error ? error.message : String(error);
2158
+ const message = userFacingErrorMessage(error, this.t);
2138
2159
  this.events.publish(session.sessionId, { type: "shell.end", output: message, isError: true }, eventScope);
2139
2160
  this.events.publish(session.sessionId, { type: "session.error", message }, eventScope);
2140
- this.publishActivity(session, "命令执行失败", "error", message);
2161
+ this.publishActivity(session, this.t("activity.commandFailed", "Command failed"), "error", message);
2141
2162
  this.publishStatus(session);
2142
2163
  });
2143
2164
  }
@@ -2153,27 +2174,27 @@ export class PiSessionService {
2153
2174
  }
2154
2175
  async navigateTree(ref, request, managementContext) {
2155
2176
  if (request.targetId.trim() === "")
2156
- throw new Error("会话树目标条目不能为空");
2177
+ throw new Error(this.t("server.main.f39f37acdac1", "Session tree target is required"));
2157
2178
  if (this.isTreeExclusiveSessionIdentityActive(sessionIdFromLookup(ref))) {
2158
- throw new Error("切换会话树前请先停止当前会话活动");
2179
+ throw new Error(this.t("server.main.e39e6b98e072", "Stop current session activity before navigating the session tree"));
2159
2180
  }
2160
2181
  await this.assertWritable(ref);
2161
- const options = sessionTreeNavigationOptions(request);
2182
+ const options = sessionTreeNavigationOptions(request, this.t);
2162
2183
  const session = await this.getOrOpen(ref, managementContext);
2163
2184
  if (typeof session.navigateTree !== "function")
2164
- throw new Error("当前 Pi 运行时不支持会话树导航");
2185
+ throw new Error(this.t("session.treeUnsupported", "Session tree navigation is not supported by this Pi runtime"));
2165
2186
  if (this.hasActiveWork(session))
2166
- throw new Error("切换会话树前请先停止当前会话活动");
2187
+ throw new Error(this.t("server.main.e39e6b98e072", "Stop current session activity before navigating the session tree"));
2167
2188
  // Acquire synchronously after the active-work check. No leaf-producing work
2168
2189
  // may enter this runtime until Pi's potentially asynchronous summary settles.
2169
2190
  this.treeNavigations.add(session);
2170
2191
  try {
2171
2192
  const oldLeafId = session.sessionManager.getLeafId();
2172
2193
  if (oldLeafId !== request.expectedLeafId) {
2173
- throw new Error("打开 /tree 后会话已发生变化。请重新打开 /tree 后重试。");
2194
+ throw new Error(this.t("session.treeStale", "The session changed since /tree was opened. Reopen /tree and try again."));
2174
2195
  }
2175
2196
  const activeEditableTargetParentId = activeEditableTreeTargetParentId(session.sessionManager, request.targetId, oldLeafId);
2176
- this.publishActivity(session, options.summarize ? "正在汇总分支" : "正在切换会话树", "active");
2197
+ this.publishActivity(session, options.summarize ? this.t("activity.summarizing", "Summarizing branch") : this.t("activity.navigating", "Navigating session tree"), "active");
2177
2198
  this.publishStatus(session);
2178
2199
  let result = await session.navigateTree(request.targetId, options);
2179
2200
  if (activeEditableTargetParentId !== undefined && !result.cancelled
@@ -2192,7 +2213,7 @@ export class PiSessionService {
2192
2213
  }
2193
2214
  if (result.cancelled) {
2194
2215
  if (this.isCurrentActiveSession(session)) {
2195
- this.publishActivity(session, result.aborted === true ? "分支汇总已终止" : "会话树切换已取消", "idle");
2216
+ this.publishActivity(session, result.aborted === true ? this.t("activity.summaryAborted", "Branch summary aborted") : this.t("activity.navigationCancelled", "Session tree navigation cancelled"), "idle");
2196
2217
  }
2197
2218
  return { cancelled: true, ...(result.aborted === undefined ? {} : { aborted: result.aborted }) };
2198
2219
  }
@@ -2212,13 +2233,13 @@ export class PiSessionService {
2212
2233
  this.unpersistedTreeBranchLeaves.set(session, selectedLeafId);
2213
2234
  }
2214
2235
  if (this.isCurrentActiveSession(session))
2215
- this.publishActivity(session, "已切换会话树", "idle");
2236
+ this.publishActivity(session, this.t("activity.navigated", "Session tree changed"), "idle");
2216
2237
  return { cancelled: false, ...(result.editorText === undefined ? {} : { editorText: result.editorText }) };
2217
2238
  }
2218
2239
  catch (error) {
2219
2240
  const message = error instanceof Error ? error.message : String(error);
2220
2241
  if (this.isCurrentActiveSession(session)) {
2221
- this.publishActivity(session, "会话树切换失败", "error", message);
2242
+ this.publishActivity(session, this.t("server.main.8cae209986b4", "tree navigation failed"), "error", message);
2222
2243
  this.events.publish(session.sessionId, { type: "session.error", message }, this.eventScopeForSession(session));
2223
2244
  }
2224
2245
  throw error;
@@ -2237,22 +2258,22 @@ export class PiSessionService {
2237
2258
  }
2238
2259
  async forkFromTree(ref, request, managementContext) {
2239
2260
  if (request.entryId.trim() === "")
2240
- throw new Error("会话树条目不能为空");
2261
+ throw new Error(this.t("server.main.f10a242f597a", "Session tree entry is required"));
2241
2262
  const requestedSessionId = sessionIdFromLookup(ref);
2242
2263
  if (this.isTreeExclusiveSessionIdentityActive(requestedSessionId)) {
2243
- throw new Error("分叉会话树前请先停止当前会话活动");
2264
+ throw new Error(this.t("server.main.3f5b7f08e91c", "Stop current session activity before forking the session tree"));
2244
2265
  }
2245
2266
  await this.assertWritable(ref);
2246
2267
  const session = await this.getOrOpen(ref, managementContext);
2247
2268
  if (this.hasActiveWork(session))
2248
- throw new Error("分叉会话树前请先停止当前会话活动");
2269
+ throw new Error(this.t("server.main.3f5b7f08e91c", "Stop current session activity before forking the session tree"));
2249
2270
  if (session.sessionManager.getLeafId() !== request.expectedLeafId) {
2250
- throw new Error("打开 /tree 后会话已发生变化。请重新打开 /tree 后重试。");
2271
+ throw new Error(this.t("session.treeStale", "The session changed since /tree was opened. Reopen /tree and try again."));
2251
2272
  }
2252
2273
  const sourceSessionId = session.sessionId;
2253
2274
  const sourceEventScope = this.eventScopeForSession(session);
2254
2275
  const sourceCwd = session.sessionManager.getCwd();
2255
- this.publishActivity(session, "正在从条目分叉会话", "active");
2276
+ this.publishActivity(session, this.t("server.main.4de911d3b72a", "forking session from entry"), "active");
2256
2277
  this.publishStatus(session);
2257
2278
  try {
2258
2279
  const result = await this.commandService.forkEntry(session.sessionId, request.entryId, sourceEventScope, {
@@ -2261,17 +2282,17 @@ export class PiSessionService {
2261
2282
  if (result.type === "unsupported")
2262
2283
  throw new Error(result.message);
2263
2284
  if (result.type !== "done")
2264
- throw new Error("会话分叉不可用");
2285
+ throw new Error(this.t("server.main.0246b6af6843", "Session fork is unavailable"));
2265
2286
  if (result.session === undefined) {
2266
2287
  if (this.isCurrentActiveSession(session)) {
2267
- this.publishActivity(session, "分叉已取消", "idle");
2288
+ this.publishActivity(session, this.t("server.main.1cceead5f414", "fork cancelled"), "idle");
2268
2289
  this.publishStatus(session);
2269
2290
  }
2270
2291
  return { cancelled: true };
2271
2292
  }
2272
2293
  const forked = this.activeForLookup({ id: result.session.id, cwd: result.session.cwd }, managementContext)?.runtime.session;
2273
2294
  if (forked !== undefined && this.isCurrentActiveSession(forked)) {
2274
- this.publishActivity(forked, "会话已分叉", "idle");
2295
+ this.publishActivity(forked, this.t("command.forked", "Session forked"), "idle");
2275
2296
  this.publishStatus(forked);
2276
2297
  }
2277
2298
  if (result.session.id !== sourceSessionId)
@@ -2285,7 +2306,7 @@ export class PiSessionService {
2285
2306
  catch (error) {
2286
2307
  const message = error instanceof Error ? error.message : String(error);
2287
2308
  if (this.isCurrentActiveSession(session)) {
2288
- this.publishActivity(session, "分叉失败", "error", message);
2309
+ this.publishActivity(session, this.t("server.main.08e8f3ea9eaa", "fork failed"), "error", message);
2289
2310
  this.events.publish(session.sessionId, { type: "session.error", message }, sourceEventScope);
2290
2311
  this.publishStatus(session);
2291
2312
  }
@@ -2296,7 +2317,7 @@ export class PiSessionService {
2296
2317
  const sessionKey = activeSessionKey(sessionId, eventScope);
2297
2318
  if (this.activities.get(sessionKey)?.phase === "active") {
2298
2319
  const at = new Date().toISOString();
2299
- const stored = { phase: "idle", label: "空闲", at };
2320
+ const stored = { phase: "idle", label: this.t("activity.idle", "Idle"), at };
2300
2321
  this.activities.set(sessionKey, stored);
2301
2322
  const activity = { sessionId, ...stored };
2302
2323
  this.events.publish(sessionId, { type: "activity.update", activity }, eventScope);
@@ -2308,7 +2329,7 @@ export class PiSessionService {
2308
2329
  async archive(ref, managementContext) {
2309
2330
  const active = await this.getActive(ref, managementContext);
2310
2331
  const session = active.runtime.session;
2311
- await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session, runtime: active.runtime }], "归档前请先停止当前会话活动", async () => {
2332
+ await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session, runtime: active.runtime }], this.t("session.stopBeforeArchive", "Stop current session activity before archiving"), async () => {
2312
2333
  const archiveInput = await this.archiveInputForSession(session);
2313
2334
  await this.closeActiveSession(active);
2314
2335
  await this.archiveStore.archive(archiveInput);
@@ -2337,7 +2358,7 @@ export class PiSessionService {
2337
2358
  const listed = findListedSessionForBulkRef(sessionContext, ref);
2338
2359
  const resolvedSessionId = active?.runtime.session.sessionId ?? listed?.id ?? ref.id;
2339
2360
  if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
2340
- failures.push({ sessionId: resolvedSessionId, error: "Stop current session activity before archiving" });
2361
+ failures.push({ sessionId: resolvedSessionId, error: this.t("session.stopBeforeArchive", "Stop current session activity before archiving") });
2341
2362
  continue;
2342
2363
  }
2343
2364
  try {
@@ -2345,21 +2366,21 @@ export class PiSessionService {
2345
2366
  planItems.push({ input: archiveInputFromListEntry(listed), ...(active === undefined ? {} : { active }) });
2346
2367
  }
2347
2368
  else if (active !== undefined) {
2348
- planItems.push({ input: archiveInputFromActiveSession(active.runtime.session), active });
2369
+ planItems.push({ input: archiveInputFromActiveSession(active.runtime.session, this.t), active });
2349
2370
  }
2350
2371
  else {
2351
- failures.push({ sessionId: ref.id, error: "Session not found" });
2372
+ failures.push({ sessionId: ref.id, error: this.t("session.notFound", "Session not found") });
2352
2373
  }
2353
2374
  }
2354
2375
  catch (error) {
2355
- failures.push({ sessionId: resolvedSessionId, error: errorMessage(error) });
2376
+ failures.push({ sessionId: resolvedSessionId, error: errorMessage(error, this.t) });
2356
2377
  }
2357
2378
  }
2358
2379
  const readyPlanItems = [];
2359
2380
  for (const item of planItems) {
2360
2381
  const active = item.active ?? this.activeForLookup(bulkRefToLookup({ id: item.input.sessionId, cwd: item.input.cwd }), managementContext);
2361
2382
  if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
2362
- failures.push({ sessionId: item.input.sessionId, error: "Stop current session activity before archiving" });
2383
+ failures.push({ sessionId: item.input.sessionId, error: this.t("session.stopBeforeArchive", "Stop current session activity before archiving") });
2363
2384
  continue;
2364
2385
  }
2365
2386
  readyPlanItems.push(active === undefined ? item : { ...item, active });
@@ -2369,7 +2390,7 @@ export class PiSessionService {
2369
2390
  await this.runTreeExclusiveOperation(readyPlanItems.map(({ input, active }) => ({
2370
2391
  sessionId: input.sessionId,
2371
2392
  ...(active === undefined ? {} : { session: active.runtime.session, runtime: active.runtime }),
2372
- })), "Stop current session activity before archiving", async () => {
2393
+ })), this.t("session.stopBeforeArchive", "Stop current session activity before archiving"), async () => {
2373
2394
  for (const item of readyPlanItems) {
2374
2395
  try {
2375
2396
  if (item.active !== undefined)
@@ -2379,7 +2400,7 @@ export class PiSessionService {
2379
2400
  readyInputs.push(item.input);
2380
2401
  }
2381
2402
  catch (error) {
2382
- failures.push({ sessionId: item.input.sessionId, error: errorMessage(error) });
2403
+ failures.push({ sessionId: item.input.sessionId, error: errorMessage(error, this.t) });
2383
2404
  }
2384
2405
  }
2385
2406
  try {
@@ -2389,7 +2410,7 @@ export class PiSessionService {
2389
2410
  }
2390
2411
  catch (error) {
2391
2412
  for (const input of readyInputs)
2392
- failures.push({ sessionId: input.sessionId, error: errorMessage(error) });
2413
+ failures.push({ sessionId: input.sessionId, error: errorMessage(error, this.t) });
2393
2414
  }
2394
2415
  });
2395
2416
  await this.forgetUnreadSessions(unreadArchivedIdentities, eventScopeFromManagementContext(managementContext));
@@ -2403,16 +2424,16 @@ export class PiSessionService {
2403
2424
  async archiveTree(ref, managementContext) {
2404
2425
  const session = await this.getOrOpen(ref, managementContext);
2405
2426
  const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
2406
- const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ?? archiveCandidateFromActiveSession(session, false);
2427
+ const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ?? archiveCandidateFromActiveSession(session, false, this.t);
2407
2428
  const plan = planSessionArchiveTree(root, catalog);
2408
2429
  const busy = plan.targets.map((target) => target.activeSession).find((target) => target !== undefined && this.hasActiveWork(target));
2409
2430
  if (busy !== undefined)
2410
- throw new Error(`归档 ${sessionDisplayName(busy)} 前请先停止当前会话活动`);
2411
- const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
2431
+ throw new Error(this.t("session.stopBeforeNamedArchive", "Stop current session activity before archiving {value0}", { value0: sessionDisplayName(busy) }));
2432
+ const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target, this.t));
2412
2433
  await this.runTreeExclusiveOperation(plan.unarchivedTargets.map((target) => ({
2413
2434
  sessionId: target.id,
2414
2435
  ...(target.activeSession === undefined ? {} : { session: target.activeSession }),
2415
- })), `归档 ${sessionDisplayName(session)} 前请先停止当前会话活动`, async () => {
2436
+ })), this.t("session.stopBeforeNamedArchive", "Stop current session activity before archiving {value0}", { value0: sessionDisplayName(session) }), async () => {
2416
2437
  for (const target of plan.targets) {
2417
2438
  if (target.archived)
2418
2439
  this.publishNotificationMutations(this.notificationsForScope(eventScopeFromManagementContext(managementContext)).clearSession(target.id, "archive"));
@@ -2432,7 +2453,7 @@ export class PiSessionService {
2432
2453
  async restore(ref, managementContext) {
2433
2454
  const archived = await this.getArchived(ref);
2434
2455
  if (archived === undefined)
2435
- throw new Error("未找到会话");
2456
+ throw new Error(this.t("session.notFound", "Session not found"));
2436
2457
  await this.closeActiveInScope(archived.sessionId, eventScopeFromManagementContext(managementContext));
2437
2458
  await this.archiveStore.restore(archived.sessionId);
2438
2459
  await this.forgetUnreadSessions([archived], eventScopeFromManagementContext(managementContext));
@@ -2440,16 +2461,16 @@ export class PiSessionService {
2440
2461
  async deleteArchived(ref, managementContext) {
2441
2462
  const record = await this.getArchived(ref);
2442
2463
  if (record === undefined)
2443
- throw new Error("未找到已归档会话");
2464
+ throw new Error(this.t("session.archiveNotFound", "Archived session not found"));
2444
2465
  if (this.archiveStore.deleteArchived === undefined)
2445
- throw new Error("归档存储不支持删除");
2466
+ throw new Error(this.t("session.archiveDeleteUnsupported", "The archive store does not support deletion"));
2446
2467
  await this.closeActiveInScope(record.sessionId, eventScopeFromManagementContext(managementContext));
2447
2468
  await this.archiveStore.deleteArchived(record.sessionId);
2448
2469
  await this.forgetUnreadSessions([record], eventScopeFromManagementContext(managementContext));
2449
2470
  }
2450
2471
  async deleteArchivedMany(refs, managementContext) {
2451
2472
  if (this.archiveStore.deleteArchived === undefined && this.archiveStore.deleteArchivedMany === undefined)
2452
- throw new Error("Archive store does not support deletion");
2473
+ throw new Error(this.t("session.archiveDeleteUnsupported", "The archive store does not support deletion"));
2453
2474
  const uniqueRefs = uniqueBulkSessionRefs(refs);
2454
2475
  const archivedRecords = await this.archiveStore.list();
2455
2476
  const failures = [];
@@ -2457,12 +2478,12 @@ export class PiSessionService {
2457
2478
  for (const ref of uniqueRefs) {
2458
2479
  const record = findArchivedRecordForBulkRef(archivedRecords, ref);
2459
2480
  if (record === undefined) {
2460
- failures.push({ sessionId: ref.id, error: "Archived session not found" });
2481
+ failures.push({ sessionId: ref.id, error: this.t("session.archiveNotFound", "Archived session not found") });
2461
2482
  continue;
2462
2483
  }
2463
2484
  const active = this.activeForLookup({ id: record.sessionId, cwd: record.cwd }, managementContext);
2464
2485
  if (active !== undefined && this.hasActiveWork(active.runtime.session)) {
2465
- failures.push({ sessionId: record.sessionId, error: "Stop current session activity before deleting archived session" });
2486
+ failures.push({ sessionId: record.sessionId, error: this.t("session.stopBeforeDeleteArchive", "Stop current session activity before deleting archived session") });
2466
2487
  continue;
2467
2488
  }
2468
2489
  planItems.push({ record });
@@ -2474,7 +2495,7 @@ export class PiSessionService {
2474
2495
  readyRecords.push(item.record);
2475
2496
  }
2476
2497
  catch (error) {
2477
- failures.push({ sessionId: item.record.sessionId, error: errorMessage(error) });
2498
+ failures.push({ sessionId: item.record.sessionId, error: errorMessage(error, this.t) });
2478
2499
  }
2479
2500
  }
2480
2501
  const deleteIds = readyRecords.map((record) => record.sessionId);
@@ -2484,7 +2505,7 @@ export class PiSessionService {
2484
2505
  }
2485
2506
  catch (error) {
2486
2507
  for (const sessionId of deleteIds)
2487
- failures.push({ sessionId, error: errorMessage(error) });
2508
+ failures.push({ sessionId, error: errorMessage(error, this.t) });
2488
2509
  }
2489
2510
  const deletedIdSet = new Set(deletedSessionIds);
2490
2511
  await this.forgetUnreadSessions(readyRecords.filter((record) => deletedIdSet.has(record.sessionId)), eventScopeFromManagementContext(managementContext));
@@ -2500,11 +2521,11 @@ export class PiSessionService {
2500
2521
  const active = await this.getActive(ref, managementContext);
2501
2522
  const session = active.runtime.session;
2502
2523
  if (this.hasActiveWork(session))
2503
- throw new Error("重新加载前请先停止当前会话活动");
2524
+ throw new Error(this.t("session.stopBeforeReload", "Stop current session activity before reloading"));
2504
2525
  const sessionFile = session.sessionFile;
2505
2526
  if (sessionFile === undefined || sessionFile === "")
2506
- throw new Error("会话尚未持久化");
2507
- const reopenedSession = await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session, runtime: active.runtime }], "重新加载前请先停止当前会话活动", async () => {
2527
+ throw new Error(this.t("session.notPersisted", "Session has not been persisted"));
2528
+ const reopenedSession = await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session, runtime: active.runtime }], this.t("session.stopBeforeReload", "Stop current session activity before reloading"), async () => {
2508
2529
  const priorGeneration = this.notificationGenerationBySession.get(session);
2509
2530
  const { sessionId, cwd } = notificationIdentityForSession(session);
2510
2531
  let candidateGeneration;
@@ -2537,7 +2558,7 @@ export class PiSessionService {
2537
2558
  const session = await this.getOrOpen(ref, managementContext);
2538
2559
  const sessionFile = session.sessionFile;
2539
2560
  if (sessionFile === undefined || sessionFile === "")
2540
- throw new Error("会话尚未持久化");
2561
+ throw new Error(this.t("session.notPersisted", "Session has not been persisted"));
2541
2562
  clearSessionFileParent(sessionFile);
2542
2563
  this.sessionManager.invalidateSessionFile(sessionFile);
2543
2564
  clearParentSessionHeader(session.sessionManager);
@@ -2553,11 +2574,11 @@ export class PiSessionService {
2553
2574
  this.abortRunScopedExtensionDialogs(active.runtime.session);
2554
2575
  try {
2555
2576
  await this.abortSessionOperations(active.runtime.session);
2556
- this.publishActivity(active.runtime.session, "已停止", "idle");
2577
+ this.publishActivity(active.runtime.session, this.t("activity.stopped", "Stopped"), "idle");
2557
2578
  }
2558
2579
  catch (error) {
2559
- const message = error instanceof Error ? error.message : String(error);
2560
- this.publishActivity(active.runtime.session, "停止失败", "error", message);
2580
+ const message = userFacingErrorMessage(error, this.t);
2581
+ this.publishActivity(active.runtime.session, this.t("activity.stopFailed", "Stop failed"), "error", message);
2561
2582
  throw error;
2562
2583
  }
2563
2584
  finally {
@@ -2607,7 +2628,7 @@ export class PiSessionService {
2607
2628
  if (this.archiveStore.deleteArchivedMany !== undefined)
2608
2629
  return this.archiveStore.deleteArchivedMany(sessionIds);
2609
2630
  if (this.archiveStore.deleteArchived === undefined)
2610
- throw new Error("Archive store does not support deletion");
2631
+ throw new Error(this.t("session.archiveDeleteUnsupported", "The archive store does not support deletion"));
2611
2632
  for (const sessionId of sessionIds)
2612
2633
  await this.archiveStore.deleteArchived(sessionId);
2613
2634
  return [...sessionIds];
@@ -2661,11 +2682,11 @@ export class PiSessionService {
2661
2682
  const cwd = session.sessionManager.getCwd();
2662
2683
  const sessionFile = session.sessionFile;
2663
2684
  if (sessionFile === undefined || sessionFile === "")
2664
- throw new Error("会话尚未持久化");
2685
+ throw new Error(this.t("session.notPersisted", "Session has not been persisted"));
2665
2686
  const listed = (await this.sessionManager.list(cwd)).find((candidate) => candidate.id === session.sessionId);
2666
2687
  if (listed !== undefined)
2667
2688
  return archiveInputFromListEntry(listed);
2668
- return archiveInputFromActiveSession(session);
2689
+ return archiveInputFromActiveSession(session, this.t);
2669
2690
  }
2670
2691
  async workspaceArchiveCandidates(cwd) {
2671
2692
  const [sessions, archivedRecords] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
@@ -2697,7 +2718,7 @@ export class PiSessionService {
2697
2718
  if (session.sessionManager.getCwd() !== cwd || archivedById.has(session.sessionId))
2698
2719
  continue;
2699
2720
  const existing = candidates.get(session.sessionId);
2700
- candidates.set(session.sessionId, { ...(existing ?? archiveCandidateFromActiveSession(session, false)), activeSession: session });
2721
+ candidates.set(session.sessionId, { ...(existing ?? archiveCandidateFromActiveSession(session, false, this.t)), activeSession: session });
2701
2722
  }
2702
2723
  return [...candidates.values()];
2703
2724
  }
@@ -2794,7 +2815,7 @@ export class PiSessionService {
2794
2815
  }
2795
2816
  async assertWritable(ref) {
2796
2817
  if (await this.getArchived(ref) !== undefined)
2797
- throw new Error("已归档会话为只读。请恢复会话后继续。");
2818
+ throw new Error(this.t("session.archiveReadOnly", "Archived sessions are read-only. Restore the session to continue."));
2798
2819
  }
2799
2820
  async getOrOpen(ref, managementContext) {
2800
2821
  return (await this.getActive(ref, managementContext)).runtime.session;
@@ -2881,7 +2902,7 @@ export class PiSessionService {
2881
2902
  ? await this.sessionManager.resolveSessionFile(ref.cwd, ref.id)
2882
2903
  : findSessionByIdOrPrefix(await this.sessionManager.listAll(), ref);
2883
2904
  if (!match)
2884
- throw new Error("未找到会话");
2905
+ throw new Error(this.t("routing.sessionNotFound", "Session not found"));
2885
2906
  return this.openPending(match.id, match.cwd, managementContext, () => this.sessionManager.open(match.path));
2886
2907
  }
2887
2908
  openPending(sessionId, cwd, managementContext, openSessionManager, options = {}) {
@@ -2949,13 +2970,13 @@ export class PiSessionService {
2949
2970
  getActiveForCommand(sessionId, eventScope) {
2950
2971
  const active = eventScope === undefined ? this.activeForSessionId(sessionId) : this.activeForSessionIdAndScope(sessionId, eventScope);
2951
2972
  if (active === undefined)
2952
- throw new Error(`Session not active: ${sessionId}`);
2973
+ throw new Error(this.t("session.notActive", "Session not active: {sessionId}", { sessionId }));
2953
2974
  return Promise.resolve(active);
2954
2975
  }
2955
2976
  submitCommandPrompt(sessionId, text, eventScope) {
2956
2977
  const active = this.activeForSessionIdAndScope(sessionId, eventScope);
2957
2978
  if (active === undefined)
2958
- throw new Error(`Session not active: ${sessionId}`);
2979
+ throw new Error(this.t("session.notActive", "Session not active: {sessionId}", { sessionId }));
2959
2980
  return this.submitPrompt(active.runtime.session, text, undefined, [], false);
2960
2981
  }
2961
2982
  /**
@@ -3011,7 +3032,7 @@ export class PiSessionService {
3011
3032
  mode: "rpc",
3012
3033
  onError: (error) => {
3013
3034
  const message = `${error.extensionPath}: ${error.error}`;
3014
- this.publishActivity(session, "扩展错误", "error", message);
3035
+ this.publishActivity(session, this.t("activity.extensionError", "Extension error"), "error", message);
3015
3036
  this.events.publish(session.sessionId, { type: "session.error", message }, owner.eventScope);
3016
3037
  },
3017
3038
  });
@@ -3047,7 +3068,7 @@ export class PiSessionService {
3047
3068
  if (eventType === "message_start" && isRecord(event) && getString(event["message"], "role") === "user") {
3048
3069
  void this.voidOpenAskForUserMessage(session).catch((error) => {
3049
3070
  const message = error instanceof Error ? error.message : String(error);
3050
- this.publishActivity(session, "扩展错误", "error", message);
3071
+ this.publishActivity(session, this.t("activity.extensionError", "Extension error"), "error", message);
3051
3072
  this.events.publish(session.sessionId, { type: "session.error", message }, active.eventScope);
3052
3073
  });
3053
3074
  }
@@ -3187,7 +3208,7 @@ export class PiSessionService {
3187
3208
  this.events.publish(session.sessionId, {
3188
3209
  type: "command.output",
3189
3210
  level: "error",
3190
- message: `Authentication for ${model.provider}/${model.id} was removed. Use /model to select another model.`,
3211
+ message: this.t("session.authRemoved", "Authentication for {provider}/{model} was removed. Use /model to select another model.", { provider: model.provider, model: model.id }),
3191
3212
  }, eventScope);
3192
3213
  }
3193
3214
  clearAuthLossWarningsForSession(sessionId) {
@@ -3227,18 +3248,18 @@ export class PiSessionService {
3227
3248
  }
3228
3249
  activityLabelFromStatus(session) {
3229
3250
  if (this.treeNavigations.has(session))
3230
- return "正在切换会话树";
3251
+ return this.t("activity.navigating", "Navigating session tree");
3231
3252
  if (this.isSessionEntryMutationActive(session))
3232
- return "正在更新会话";
3253
+ return this.t("activity.updating", "Updating session");
3233
3254
  if (session.isCompacting)
3234
- return "正在压缩上下文";
3255
+ return this.t("activity.compacting", "Compacting context");
3235
3256
  if (session.isBashRunning)
3236
- return "正在运行命令";
3257
+ return this.t("activity.commandRunning", "Running command");
3237
3258
  if (session.isStreaming)
3238
- return "代理正在运行";
3259
+ return this.t("activity.agentRunning", "Agent running");
3239
3260
  if (this.pendingMessageCount(session) > 0)
3240
- return "正在排队";
3241
- return "正在处理";
3261
+ return this.t("activity.queued", "Queued");
3262
+ return this.t("activity.working", "Working");
3242
3263
  }
3243
3264
  hasActiveWork(session) {
3244
3265
  return this.treeNavigations.has(session)
@@ -3304,7 +3325,7 @@ export class PiSessionService {
3304
3325
  }
3305
3326
  assertTreeNavigationInactive(session, action) {
3306
3327
  if (this.treeNavigations.has(session))
3307
- throw new Error(`会话树导航期间无法${action}`);
3328
+ throw new Error(this.t("server.main.0c70757b6815", "Cannot {arg0} while session tree navigation is active", { arg0: action }));
3308
3329
  }
3309
3330
  async runSessionEntryMutation(session, action, operation) {
3310
3331
  this.beginSessionEntryMutation(session, action);
@@ -3336,56 +3357,56 @@ export class PiSessionService {
3336
3357
  if (eventType === undefined)
3337
3358
  return;
3338
3359
  if (eventType === "agent_start") {
3339
- this.publishActivity(session, "代理正在运行", "active");
3360
+ this.publishActivity(session, this.t("activity.agentRunning", "Agent running"), "active");
3340
3361
  return;
3341
3362
  }
3342
3363
  if (eventType === "agent_end") {
3343
- this.publishActivity(session, "空闲", "idle");
3364
+ this.publishActivity(session, this.t("activity.idle", "Idle"), "idle");
3344
3365
  const eventScope = this.eventScopeForSession(session);
3345
3366
  const sessionKey = activeSessionKey(session.sessionId, eventScope);
3346
3367
  this.activities.scheduleSettledRefresh(sessionKey, () => {
3347
3368
  if (this.active.get(sessionKey)?.runtime.session !== session)
3348
3369
  return;
3349
- this.publishActivity(session, "空闲", "idle");
3370
+ this.publishActivity(session, this.t("activity.idle", "Idle"), "idle");
3350
3371
  this.publishStatus(session);
3351
3372
  }, 250);
3352
3373
  return;
3353
3374
  }
3354
3375
  if (eventType === "turn_end") {
3355
- this.publishActivity(session, "本轮已完成", "idle");
3376
+ this.publishActivity(session, this.t("activity.turnCompleted", "Turn completed"), "idle");
3356
3377
  return;
3357
3378
  }
3358
3379
  if (eventType === "message_start") {
3359
- this.publishActivity(session, "消息开始处理", "active");
3380
+ this.publishActivity(session, this.t("activity.messageStarted", "Message started"), "active");
3360
3381
  return;
3361
3382
  }
3362
3383
  if (eventType === "message_end") {
3363
- this.publishActivity(session, "消息处理完成", "idle");
3384
+ this.publishActivity(session, this.t("activity.messageCompleted", "Message completed"), "idle");
3364
3385
  return;
3365
3386
  }
3366
3387
  if (eventType === "message_update") {
3367
- this.publishActivity(session, "正在接收回复", "active");
3388
+ this.publishActivity(session, this.t("activity.receiving", "Receiving reply"), "active");
3368
3389
  return;
3369
3390
  }
3370
3391
  if (eventType === "tool_execution_start") {
3371
- this.publishActivity(session, "正在运行工具", "active", getString(event, "toolName"));
3392
+ this.publishActivity(session, this.t("activity.toolRunning", "Running tool"), "active", getString(event, "toolName"));
3372
3393
  return;
3373
3394
  }
3374
3395
  if (eventType === "tool_execution_end") {
3375
3396
  const isError = getBoolean(event, "isError") === true;
3376
- this.publishActivity(session, isError ? "工具执行失败" : "工具执行完成", isError ? "error" : "idle", getString(event, "toolName"));
3397
+ this.publishActivity(session, isError ? this.t("activity.toolFailed", "Tool failed") : this.t("activity.toolCompleted", "Tool completed"), isError ? "error" : "idle", getString(event, "toolName"));
3377
3398
  return;
3378
3399
  }
3379
3400
  if (eventType === "bash_execution_start") {
3380
- this.publishActivity(session, "正在运行命令", "active");
3401
+ this.publishActivity(session, this.t("activity.commandRunning", "Running command"), "active");
3381
3402
  return;
3382
3403
  }
3383
3404
  if (eventType === "bash_execution_end") {
3384
- this.publishActivity(session, "命令执行完成", "idle");
3405
+ this.publishActivity(session, this.t("activity.commandCompleted", "Command completed"), "idle");
3385
3406
  return;
3386
3407
  }
3387
3408
  if (this.hasActiveWork(session))
3388
- this.publishActivity(session, "正在处理", "active");
3409
+ this.publishActivity(session, this.t("activity.working", "Working"), "active");
3389
3410
  }
3390
3411
  auditToolExecutionEvent(active, event) {
3391
3412
  const eventType = getString(event, "type");
@@ -3482,7 +3503,7 @@ export class PiSessionService {
3482
3503
  const sessionId = sessionManager.getSessionId();
3483
3504
  if (sessionId === "")
3484
3505
  return { report: noop, end: noop };
3485
- const label = intent === "create" ? "正在创建会话" : "正在打开会话";
3506
+ const label = intent === "create" ? this.t("activity.creating", "Creating session") : this.t("activity.opening", "Opening session");
3486
3507
  return {
3487
3508
  report: (phase) => { this.publishStartupProgress(sessionId, startupToken, label, "active", this.startupDetail(phase), eventScope); },
3488
3509
  end: () => {
@@ -3490,13 +3511,13 @@ export class PiSessionService {
3490
3511
  // is the truth about this session and must survive the clear.
3491
3512
  if (this.activities.get(activeSessionKey(sessionId, eventScope)) !== undefined)
3492
3513
  return;
3493
- this.publishStartupProgress(sessionId, startupToken, "空闲", "idle", undefined, eventScope);
3514
+ this.publishStartupProgress(sessionId, startupToken, this.t("activity.idle", "Idle"), "idle", undefined, eventScope);
3494
3515
  },
3495
3516
  };
3496
3517
  }
3497
3518
  startupDetail(phase) {
3498
3519
  return this.catalogRefreshStatus?.isRefreshInFlight() === true
3499
- ? `${phase} · ${STARTUP_CONCURRENT_CATALOG_REFRESH}`
3520
+ ? `${phase} · ${this.t("startup.catalogRefresh", "Provider model catalog is refreshing")}`
3500
3521
  : phase;
3501
3522
  }
3502
3523
  /**
@@ -3556,7 +3577,7 @@ export class PiSessionService {
3556
3577
  if (current?.phase !== "active" || this.hasActiveWork(session))
3557
3578
  return;
3558
3579
  const at = new Date().toISOString();
3559
- const stored = { phase: "idle", label: "空闲", at };
3580
+ const stored = { phase: "idle", label: this.t("activity.idle", "Idle"), at };
3560
3581
  this.activities.set(activeSessionKey(session.sessionId, eventScope), stored);
3561
3582
  const activity = { sessionId: session.sessionId, ...stored };
3562
3583
  this.events.publish(session.sessionId, { type: "activity.update", activity }, eventScope);
@@ -3635,7 +3656,7 @@ export class PiSessionService {
3635
3656
  && this.activityMarker.isActiveElsewhere(session.sessionFile)) {
3636
3657
  warnings.push({
3637
3658
  severity: "info",
3638
- message: "此会话最近在另一个 PI WEB 实例中活动。请避免在两个实例中同时操作此会话。",
3659
+ message: this.t("server.main.b80808e6ef11", "Recently active in another PI-WEB instance. Avoid working on this session in both instances at once."),
3639
3660
  source: "PI-WEB",
3640
3661
  });
3641
3662
  }
@@ -3715,7 +3736,7 @@ export class PiSessionService {
3715
3736
  return result.snapshot;
3716
3737
  }
3717
3738
  async deliverSubsessionNotification(session, notification) {
3718
- await this.runSessionEntryMutation(session, "发送子会话通知", () => session.sendCustomMessage({ customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE, content: notification.text, display: true, details: { sessionId: notification.childId } }, { triggerTurn: true, deliverAs: "followUp" }));
3739
+ await this.runSessionEntryMutation(session, this.t("session.action.notify", "deliver a subsession notification"), () => session.sendCustomMessage({ customType: SUBSESSION_NOTIFICATION_CUSTOM_TYPE, content: notification.text, display: true, details: { sessionId: notification.childId } }, { triggerTurn: true, deliverAs: "followUp" }));
3719
3740
  this.publishStatus(session);
3720
3741
  }
3721
3742
  logSubsessionNotificationFailure(parentId, childId, error) {
@@ -3723,9 +3744,9 @@ export class PiSessionService {
3723
3744
  }
3724
3745
  async reloadSessionRuntime(session) {
3725
3746
  if (this.hasActiveWork(session))
3726
- throw new Error("Stop current session activity before reloading");
3727
- await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], "Stop current session activity before reloading", async () => {
3728
- this.publishActivity(session, "正在重新加载资源", "active");
3747
+ throw new Error(this.t("session.stopBeforeReload", "Stop current session activity before reloading"));
3748
+ await this.runTreeExclusiveOperation([{ sessionId: session.sessionId, session }], this.t("session.stopBeforeReload", "Stop current session activity before reloading"), async () => {
3749
+ this.publishActivity(session, this.t("activity.reloading", "Reloading resources"), "active");
3729
3750
  const priorGeneration = this.notificationGenerationBySession.get(session);
3730
3751
  let candidateGeneration;
3731
3752
  try {
@@ -3740,7 +3761,7 @@ export class PiSessionService {
3740
3761
  if (candidateGeneration !== undefined) {
3741
3762
  this.publishNotificationMutations(this.notificationsForSession(session).commitReplacement(candidateGeneration));
3742
3763
  }
3743
- this.publishActivity(session, "资源已重新加载", "idle");
3764
+ this.publishActivity(session, this.t("activity.reloaded", "Resources reloaded"), "idle");
3744
3765
  this.publishStatus(session);
3745
3766
  }
3746
3767
  catch (error) {
@@ -3749,7 +3770,7 @@ export class PiSessionService {
3749
3770
  this.notificationGenerationBySession.set(session, candidateGeneration);
3750
3771
  }
3751
3772
  const message = error instanceof Error ? error.message : String(error);
3752
- this.publishActivity(session, "重新加载失败", "error", message);
3773
+ this.publishActivity(session, this.t("activity.reloadFailed", "Reload failed"), "error", message);
3753
3774
  this.events.publish(session.sessionId, { type: "session.error", message }, this.eventScopeForSession(session));
3754
3775
  this.publishStatus(session);
3755
3776
  throw error;
@@ -3769,7 +3790,7 @@ export class PiSessionService {
3769
3790
  return [...this.active.values()].some((active) => active.runtime.session === session);
3770
3791
  }
3771
3792
  async createSessionRuntime(sessionManager, cwd, options, startup) {
3772
- startup.report(STARTUP_PHASE_RUNTIME);
3793
+ startup.report(this.t("startup.runtime", "Loading session runtime"));
3773
3794
  options.startupSignal?.throwIfAborted();
3774
3795
  // Ordinary local sessions (undefined provenance) keep the current rule.
3775
3796
  // Only an explicit host one-shot forces delegation off; do not treat every
@@ -3836,7 +3857,7 @@ export class PiSessionService {
3836
3857
  await this.recoverSubsessionTrackingForOpenedSession(runtime.session);
3837
3858
  }
3838
3859
  startupSignal?.throwIfAborted();
3839
- startup.report(STARTUP_PHASE_EXTENSIONS);
3860
+ startup.report(this.t("startup.extensions", "Starting session extensions"));
3840
3861
  // Startup hooks emit through the same scoped stream as hosted turns.
3841
3862
  this.bindRuntime(active);
3842
3863
  await this.bindSessionExtensions(runtime.session, notificationGeneration, active, startupSignal);
@@ -3992,7 +4013,7 @@ export class PiSessionService {
3992
4013
  await this.publishUnreadMutations(mutations);
3993
4014
  }
3994
4015
  observeUnreadActivityState(session) {
3995
- const mutations = this.unreadForScope(this.eventScopeForSession(session)).observeActivityState(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()), this.hasActiveWork(session));
4016
+ const mutations = this.unreadForScope(this.eventScopeForSession(session)).observeActivityState(session.sessionId, canonicalizeStoredCwd(session.sessionManager.getCwd()), this.hasActiveWork(session), () => ({ messageCount: historyMessages(session).length, leafId: session.sessionManager.getLeafId() }));
3996
4017
  if (mutations.length === 0)
3997
4018
  return;
3998
4019
  void this.publishUnreadMutations(mutations).catch(() => undefined);
@@ -4143,8 +4164,8 @@ function findSessionByIdOrPrefix(sessions, sessionId) {
4143
4164
  function uniqueStrings(values) {
4144
4165
  return [...new Set(values)];
4145
4166
  }
4146
- function errorMessage(error) {
4147
- return error instanceof Error ? error.message : String(error);
4167
+ function errorMessage(error, t = createTranslator("en", {})) {
4168
+ return userFacingErrorMessage(error, t);
4148
4169
  }
4149
4170
  function modelToClientModel(model) {
4150
4171
  if (model === undefined)