@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.7

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 (244) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
  3. package/dist/cli.js +3342 -3274
  4. package/dist/types/capability/index.d.ts +1 -1
  5. package/dist/types/capability/types.d.ts +23 -1
  6. package/dist/types/config/settings-schema.d.ts +39 -2
  7. package/dist/types/cursor.d.ts +2 -1
  8. package/dist/types/extensibility/extensions/runner.d.ts +4 -0
  9. package/dist/types/extensibility/shared-events.d.ts +18 -1
  10. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  11. package/dist/types/internal-urls/parse.d.ts +12 -0
  12. package/dist/types/internal-urls/router.d.ts +6 -0
  13. package/dist/types/internal-urls/types.d.ts +6 -0
  14. package/dist/types/lsp/config.d.ts +1 -0
  15. package/dist/types/lsp/types.d.ts +2 -0
  16. package/dist/types/mcp/manager.d.ts +5 -0
  17. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +5 -0
  19. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  20. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  21. package/dist/types/sdk.d.ts +3 -1
  22. package/dist/types/session/agent-session-types.d.ts +8 -6
  23. package/dist/types/session/agent-session.d.ts +28 -1
  24. package/dist/types/session/model-controls.d.ts +4 -1
  25. package/dist/types/session/session-advisors.d.ts +7 -1
  26. package/dist/types/session/session-tools.d.ts +44 -6
  27. package/dist/types/session/streaming-output.d.ts +7 -2
  28. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  29. package/dist/types/session/turn-recovery.d.ts +5 -3
  30. package/dist/types/task/index.d.ts +1 -1
  31. package/dist/types/task/types.d.ts +3 -11
  32. package/dist/types/thinking.d.ts +21 -2
  33. package/dist/types/tools/index.d.ts +8 -3
  34. package/dist/types/tools/output-meta.d.ts +5 -0
  35. package/dist/types/tools/read.d.ts +9 -1
  36. package/dist/types/tools/xdev.d.ts +53 -67
  37. package/dist/types/utils/cpuprofile.d.ts +51 -0
  38. package/dist/types/utils/inspect-image-mode.d.ts +29 -0
  39. package/dist/types/utils/profile-tree.d.ts +47 -0
  40. package/dist/types/utils/sample-profile.d.ts +67 -0
  41. package/dist/types/utils/title-generator.d.ts +17 -16
  42. package/package.json +12 -12
  43. package/src/capability/index.ts +43 -12
  44. package/src/capability/mcp.ts +21 -0
  45. package/src/capability/types.ts +20 -1
  46. package/src/cli/read-cli.ts +44 -2
  47. package/src/config/settings-schema.ts +42 -2
  48. package/src/config/settings.ts +35 -0
  49. package/src/cursor.ts +4 -3
  50. package/src/discovery/builtin-rules/index.ts +2 -0
  51. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  52. package/src/eval/py/runner.py +16 -2
  53. package/src/extensibility/extensions/runner.ts +117 -5
  54. package/src/extensibility/extensions/types.ts +0 -1
  55. package/src/extensibility/extensions/wrapper.ts +74 -42
  56. package/src/extensibility/hooks/tool-wrapper.ts +11 -4
  57. package/src/extensibility/hooks/types.ts +0 -1
  58. package/src/extensibility/shared-events.ts +18 -1
  59. package/src/internal-urls/mcp-protocol.ts +17 -3
  60. package/src/internal-urls/parse.ts +31 -0
  61. package/src/internal-urls/router.ts +24 -4
  62. package/src/internal-urls/types.ts +6 -0
  63. package/src/live/transport.ts +2 -2
  64. package/src/lsp/client.ts +2 -2
  65. package/src/lsp/config.ts +4 -0
  66. package/src/lsp/types.ts +2 -0
  67. package/src/mcp/config.ts +26 -14
  68. package/src/mcp/manager.ts +26 -9
  69. package/src/mcp/tool-bridge.ts +52 -1
  70. package/src/memories/index.ts +25 -6
  71. package/src/modes/components/custom-editor.ts +39 -16
  72. package/src/modes/components/status-line/segments.ts +3 -1
  73. package/src/modes/components/tips.txt +2 -1
  74. package/src/modes/components/tool-execution.ts +8 -7
  75. package/src/modes/controllers/command-controller.ts +10 -10
  76. package/src/modes/controllers/extension-ui-controller.ts +7 -7
  77. package/src/modes/controllers/selector-controller.ts +7 -2
  78. package/src/modes/rpc/rpc-mode.ts +60 -47
  79. package/src/prompts/steering/user-interjection.md +2 -5
  80. package/src/prompts/tools/task.md +4 -2
  81. package/src/sdk.ts +64 -58
  82. package/src/session/agent-session-types.ts +8 -5
  83. package/src/session/agent-session.ts +144 -11
  84. package/src/session/model-controls.ts +48 -12
  85. package/src/session/session-advisors.ts +30 -14
  86. package/src/session/session-listing.ts +66 -4
  87. package/src/session/session-tools.ts +162 -54
  88. package/src/session/streaming-output.ts +18 -6
  89. package/src/session/tool-choice-queue.ts +19 -4
  90. package/src/session/turn-recovery.ts +25 -6
  91. package/src/slash-commands/builtin-registry.ts +69 -0
  92. package/src/task/executor.ts +11 -3
  93. package/src/task/index.ts +43 -32
  94. package/src/task/types.ts +12 -17
  95. package/src/thinking.ts +68 -5
  96. package/src/tools/bash.ts +16 -9
  97. package/src/tools/index.ts +36 -16
  98. package/src/tools/output-meta.ts +20 -0
  99. package/src/tools/read.ts +89 -15
  100. package/src/tools/write.ts +16 -7
  101. package/src/tools/xdev.ts +198 -210
  102. package/src/utils/cpuprofile.ts +235 -0
  103. package/src/utils/inspect-image-mode.ts +39 -0
  104. package/src/utils/profile-tree.ts +111 -0
  105. package/src/utils/sample-profile.ts +437 -0
  106. package/src/utils/title-generator.ts +88 -34
  107. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  108. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  109. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  110. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  111. package/dist/types/cli/update-cli.test.d.ts +0 -1
  112. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  113. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  114. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  115. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  116. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  117. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  118. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  119. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  120. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  121. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  122. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  123. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  124. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  125. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  126. package/dist/types/hindsight/client.test.d.ts +0 -1
  127. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  128. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  129. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  130. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  131. package/dist/types/launch/protocol.test.d.ts +0 -1
  132. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  133. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  134. package/dist/types/live/protocol.test.d.ts +0 -1
  135. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  136. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  137. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  138. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  139. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  140. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  141. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  142. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  143. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  144. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  145. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  146. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  147. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  148. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  149. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  150. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  151. package/dist/types/modes/print-mode.test.d.ts +0 -1
  152. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  153. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  154. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  155. package/dist/types/modes/warp-events.test.d.ts +0 -1
  156. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  157. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  158. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  159. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  160. package/dist/types/session/blob-store.test.d.ts +0 -1
  161. package/dist/types/session/messages.test.d.ts +0 -1
  162. package/dist/types/session/session-context.test.d.ts +0 -1
  163. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  164. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  165. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  166. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  167. package/dist/types/system-prompt.test.d.ts +0 -1
  168. package/dist/types/task/render.test.d.ts +0 -1
  169. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  170. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  171. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  172. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  173. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  174. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  175. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  176. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  177. package/src/advisor/__tests__/config.test.ts +0 -349
  178. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  179. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  180. package/src/cli/update-cli.test.ts +0 -28
  181. package/src/config/__tests__/model-registry.test.ts +0 -182
  182. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  183. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  184. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  185. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  186. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  187. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  188. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  189. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  190. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  191. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  192. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  193. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  194. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  195. package/src/hindsight/client.test.ts +0 -75
  196. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  197. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  198. package/src/launch/broker-list-order.test.ts +0 -89
  199. package/src/launch/broker-output-snapshot.test.ts +0 -126
  200. package/src/launch/protocol.test.ts +0 -59
  201. package/src/launch/spawn-options.test.ts +0 -31
  202. package/src/launch/terminal-output.test.ts +0 -107
  203. package/src/live/protocol.test.ts +0 -140
  204. package/src/mcp/config-writer.test.ts +0 -43
  205. package/src/mcp/smithery-auth.test.ts +0 -29
  206. package/src/mcp/smithery-registry.test.ts +0 -51
  207. package/src/mcp/transports/stdio.test.ts +0 -427
  208. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  209. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  210. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  211. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  212. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  213. package/src/modes/components/custom-editor.test.ts +0 -510
  214. package/src/modes/components/login-dialog.test.ts +0 -56
  215. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  216. package/src/modes/components/status-line/component.test.ts +0 -84
  217. package/src/modes/components/tool-execution.test.ts +0 -162
  218. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  219. package/src/modes/noninteractive-dispose.test.ts +0 -73
  220. package/src/modes/print-mode.test.ts +0 -71
  221. package/src/modes/session-teardown.test.ts +0 -219
  222. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  223. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  224. package/src/modes/warp-events.test.ts +0 -794
  225. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  226. package/src/plan-mode/model-transition.test.ts +0 -60
  227. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  228. package/src/session/agent-session-error-log.test.ts +0 -59
  229. package/src/session/blob-store.test.ts +0 -56
  230. package/src/session/messages.test.ts +0 -282
  231. package/src/session/session-context.test.ts +0 -384
  232. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  233. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  234. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  235. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  236. package/src/system-prompt.test.ts +0 -236
  237. package/src/task/render.test.ts +0 -290
  238. package/src/task/spawn-policy.test.ts +0 -62
  239. package/src/tools/__tests__/eval-description.test.ts +0 -18
  240. package/src/tools/__tests__/glob.test.ts +0 -37
  241. package/src/tools/__tests__/json-tree.test.ts +0 -35
  242. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  243. package/src/tools/hub/launch-compat.test.ts +0 -40
  244. package/src/vibe/__tests__/token-rate.test.ts +0 -96
package/src/lsp/config.ts CHANGED
@@ -74,6 +74,8 @@ function normalizeServerConfig(name: string, config: RawServerConfig): ServerCon
74
74
  const fileTypes =
75
75
  normalizeStringArray(config.fileTypes) ?? normalizeExtensionToFileTypes(config.extensionToLanguage);
76
76
  const rootMarkers = normalizeStringArray(config.rootMarkers) ?? (config.extensionToLanguage ? ["."] : null);
77
+ const languageId =
78
+ typeof config.languageId === "string" && config.languageId.length > 0 ? config.languageId : undefined;
77
79
 
78
80
  if (!command || !fileTypes || !rootMarkers) {
79
81
  logger.warn("Ignoring invalid LSP server config (missing required fields).", { name });
@@ -95,6 +97,7 @@ function normalizeServerConfig(name: string, config: RawServerConfig): ServerCon
95
97
  args,
96
98
  fileTypes,
97
99
  rootMarkers,
100
+ languageId,
98
101
  ...(initOptions ? { initOptions } : {}),
99
102
  };
100
103
  }
@@ -423,6 +426,7 @@ function getConfigSources(cwd: string): ConfigSource[] {
423
426
  * "command": "/path/to/server",
424
427
  * "args": ["--stdio"],
425
428
  * "fileTypes": [".xyz"],
429
+ * "languageId": "xyz",
426
430
  * "rootMarkers": [".xyz-project"]
427
431
  * }
428
432
  * }
package/src/lsp/types.ts CHANGED
@@ -340,6 +340,8 @@ export interface ServerConfig {
340
340
  command: string;
341
341
  args?: string[];
342
342
  fileTypes: string[];
343
+ /** LSP language identifier sent in didOpen; inferred from the file path when omitted. */
344
+ languageId?: string;
343
345
  rootMarkers: string[];
344
346
  initOptions?: Record<string, unknown>;
345
347
  settings?: Record<string, unknown>;
package/src/mcp/config.ts CHANGED
@@ -97,14 +97,6 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
97
97
  const filterExa = options?.filterExa ?? true;
98
98
  const filterBrowser = options?.filterBrowser ?? false;
99
99
 
100
- // Load MCP servers via capability system
101
- const result = await loadCapability<MCPServer>(mcpCapability.id, { cwd });
102
-
103
- // Filter out project-level configs if disabled
104
- const servers = enableProjectConfig
105
- ? result.items
106
- : result.items.filter(server => server._source.level !== "project");
107
-
108
100
  // Load user-level disable/force-enable lists. The denylist always wins; the
109
101
  // allowlist overrides a non-writable source config's `enabled: false`.
110
102
  const userPath = getMCPConfigPath("user", cwd);
@@ -112,14 +104,34 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
112
104
  readDisabledServers(userPath).then(list => new Set(list)),
113
105
  readEnabledServers(userPath).then(list => new Set(list)),
114
106
  ]);
115
- // Convert to legacy format and preserve source metadata
107
+
108
+ // Scope exclusions drop entries entirely BEFORE deduplication: with project
109
+ // config disabled, a project entry must not shadow anything.
110
+ const includeServer = (server: MCPServer & { _source: SourceMeta }): boolean =>
111
+ enableProjectConfig || server._source.level !== "project";
112
+
113
+ // Disabled servers are suppressed rather than dropped: they still own their
114
+ // name at key-level dedupe (a disabled project `foo` keeps a same-named,
115
+ // lower-priority user `foo` disabled), but never equivalence-shadow a
116
+ // differently-named enabled server — otherwise the disabled alias would be
117
+ // removed downstream and starve the surviving connection.
118
+ const suppressServer = (server: MCPServer & { _source: SourceMeta }): boolean => {
119
+ if (disabledServers.has(server.name)) return true;
120
+ if (server.enabled === false && !forcedEnabled.has(server.name)) return true;
121
+ return false;
122
+ };
123
+
124
+ const result = await loadCapability<MCPServer>(mcpCapability.id, {
125
+ cwd,
126
+ filter: includeServer,
127
+ suppress: suppressServer,
128
+ });
129
+
130
+ // Convert to legacy format and preserve source metadata.
116
131
  let configs: Record<string, MCPServerConfig> = {};
117
132
  let sources: Record<string, SourceMeta> = {};
118
- for (const server of servers) {
119
- const config = convertToLegacyConfig(server);
120
- if (disabledServers.has(server.name)) continue;
121
- if (config.enabled === false && !forcedEnabled.has(server.name)) continue;
122
- configs[server.name] = config;
133
+ for (const server of result.items) {
134
+ configs[server.name] = convertToLegacyConfig(server);
123
135
  sources[server.name] = server._source;
124
136
  }
125
137
 
@@ -1041,13 +1041,7 @@ export class MCPManager {
1041
1041
  async #loadServerResourcesAndPrompts(name: string, connection: MCPServerConnection): Promise<void> {
1042
1042
  if (serverSupportsResources(connection.capabilities)) {
1043
1043
  try {
1044
- const [resources] = await Promise.all([listResources(connection), listResourceTemplates(connection)]);
1045
-
1046
- if (this.#notificationsEnabled && connection.capabilities.resources?.subscribe) {
1047
- const uris = resources.map(r => r.uri);
1048
- const notificationEpoch = this.#notificationsEpoch;
1049
- this.#subscribeAndTrack(name, connection, uris, notificationEpoch);
1050
- }
1044
+ await this.refreshServerResources(name);
1051
1045
  } catch (error) {
1052
1046
  logger.debug("Failed to load MCP resources", { path: `mcp:${name}`, error });
1053
1047
  }
@@ -1107,8 +1101,20 @@ export class MCPManager {
1107
1101
  connection.resources = undefined;
1108
1102
  connection.resourceTemplates = undefined;
1109
1103
 
1110
- // Reload
1111
- const [resources] = await Promise.all([listResources(connection), listResourceTemplates(connection)]);
1104
+ // Reload. Template listing failures must not discard a successful
1105
+ // resources/list let both settle, then continue without templates.
1106
+ const [resourcesResult, templatesResult] = await Promise.allSettled([
1107
+ listResources(connection),
1108
+ listResourceTemplates(connection),
1109
+ ]);
1110
+ if (templatesResult.status === "rejected") {
1111
+ logger.debug("Failed to list MCP resource templates", {
1112
+ path: `mcp:${name}`,
1113
+ error: templatesResult.reason,
1114
+ });
1115
+ }
1116
+ if (resourcesResult.status === "rejected") throw resourcesResult.reason;
1117
+ const resources = resourcesResult.value;
1112
1118
  if (this.#notificationsEnabled && connection.capabilities.resources?.subscribe) {
1113
1119
  const newUris = new Set(resources.map(r => r.uri));
1114
1120
  const oldUris = this.#subscribedResources.get(name);
@@ -1161,6 +1167,17 @@ export class MCPManager {
1161
1167
  return promise;
1162
1168
  }
1163
1169
 
1170
+ /**
1171
+ * Wait until a connected server's resource catalog has been loaded.
1172
+ * Coalesces with initial loading and notification-driven refreshes.
1173
+ */
1174
+ async ensureServerResources(name: string): Promise<void> {
1175
+ const connection = this.#connections.get(name);
1176
+ if (!connection || !serverSupportsResources(connection.capabilities)) return;
1177
+ if (connection.resources !== undefined && connection.resourceTemplates !== undefined) return;
1178
+ await this.refreshServerResources(name);
1179
+ }
1180
+
1164
1181
  /**
1165
1182
  * Refresh prompts from a specific server.
1166
1183
  */
@@ -6,7 +6,7 @@
6
6
  import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
7
7
  import type { TSchema } from "@oh-my-pi/pi-ai";
8
8
  import { normalizeSchemaForMCP } from "@oh-my-pi/pi-ai/utils/schema";
9
- import { untilAborted } from "@oh-my-pi/pi-utils";
9
+ import { logger, untilAborted } from "@oh-my-pi/pi-utils";
10
10
  import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
11
11
  import type { SourceMeta } from "../capability/types";
12
12
  import type {
@@ -357,6 +357,57 @@ export function createMCPToolName(serverName: string, toolName: string): string
357
357
  return `mcp__${sanitizedServerName}_${normalizedToolName}`;
358
358
  }
359
359
 
360
+ /**
361
+ * Keeps one MCP tool per minted name and logs collisions between distinct MCP
362
+ * origins. The winner is chosen by a stable origin key (server name + original
363
+ * tool name), NOT array order: MCPManager re-appends a reconnecting server's
364
+ * tools, so insertion order is mutable across reconnects and first-wins would
365
+ * silently flip ownership of the minted name. Non-MCP tools pass through
366
+ * unchanged.
367
+ */
368
+ export function deduplicateMCPToolsByName<T extends { name: string; mcpServerName?: unknown; mcpToolName?: unknown }>(
369
+ tools: readonly T[],
370
+ ): T[] {
371
+ const deduplicated: T[] = [];
372
+ const registered = new Map<string, { tool: T; originKey: string; index: number }>();
373
+
374
+ for (const tool of tools) {
375
+ if (typeof tool.mcpServerName !== "string" || typeof tool.mcpToolName !== "string") {
376
+ deduplicated.push(tool);
377
+ continue;
378
+ }
379
+
380
+ const originKey = `${tool.mcpServerName}\u0000${tool.mcpToolName}`;
381
+ const existing = registered.get(tool.name);
382
+ if (!existing) {
383
+ registered.set(tool.name, { tool, originKey, index: deduplicated.length });
384
+ deduplicated.push(tool);
385
+ continue;
386
+ }
387
+
388
+ if (existing.originKey === originKey) continue;
389
+
390
+ // Deterministic winner regardless of encounter order across reconnects.
391
+ const keepExisting = existing.originKey < originKey;
392
+ const winner = keepExisting ? existing.tool : tool;
393
+ const loser = keepExisting ? tool : existing.tool;
394
+ if (!keepExisting) {
395
+ deduplicated[existing.index] = tool;
396
+ existing.tool = tool;
397
+ existing.originKey = originKey;
398
+ }
399
+ logger.warn("MCP tool name collision; keeping stable winner", {
400
+ name: tool.name,
401
+ keptServer: winner.mcpServerName,
402
+ keptTool: winner.mcpToolName,
403
+ ignoredServer: loser.mcpServerName,
404
+ ignoredTool: loser.mcpToolName,
405
+ });
406
+ }
407
+
408
+ return deduplicated;
409
+ }
410
+
360
411
  /**
361
412
  * Parse an MCP tool name back to server and tool components.
362
413
  *
@@ -1361,12 +1361,31 @@ async function appendLearnedLine(filePath: string, line: string): Promise<void>
1361
1361
  } catch (err) {
1362
1362
  if (!isEnoent(err)) throw err;
1363
1363
  }
1364
- const prior = existing
1365
- .split("\n")
1366
- .map(l => l.trim())
1367
- .filter(l => l.startsWith("- ") && l !== line);
1368
- const lessons = [line, ...prior].slice(0, MAX_LEARNED_LESSONS);
1369
- await Bun.write(filePath, `${lessons.join("\n")}\n`);
1364
+ // Treat the file as an ordered line list so headings, prose, and blank
1365
+ // lines keep their positions relative to the bullets they scope. Managed
1366
+ // operations touch only bullet lines: dedupe removes an existing copy of
1367
+ // the incoming lesson in place, the new lesson enters at the head of the
1368
+ // first bullet run (newest-first, matching the read path and cap docs),
1369
+ // and the cap drops the oldest (bottom-most) bullets. Hand-edited content
1370
+ // outside the list region survives every write byte-for-byte.
1371
+ const lines = existing.split("\n");
1372
+ // A well-formed file ends with "\n"; drop the terminal split artifact so
1373
+ // repeated saves stay idempotent instead of growing a blank line each time.
1374
+ if (lines.at(-1) === "") lines.pop();
1375
+ const isLesson = (l: string) => l.trimStart().startsWith("- ");
1376
+ const out = lines.filter(l => !(isLesson(l) && l.trim() === line));
1377
+ const firstBullet = out.findIndex(isLesson);
1378
+ if (firstBullet === -1) out.push(line);
1379
+ else out.splice(firstBullet, 0, line);
1380
+ let lessonCount = 0;
1381
+ for (const l of out) if (isLesson(l)) lessonCount++;
1382
+ for (let i = out.length - 1; i >= 0 && lessonCount > MAX_LEARNED_LESSONS; i--) {
1383
+ if (isLesson(out[i])) {
1384
+ out.splice(i, 1);
1385
+ lessonCount--;
1386
+ }
1387
+ }
1388
+ await Bun.write(filePath, `${out.join("\n")}\n`);
1370
1389
  }
1371
1390
 
1372
1391
  /**
@@ -71,6 +71,14 @@ function buildMatchKeys(keys: readonly KeyId[]): Set<string> {
71
71
  return matchKeys;
72
72
  }
73
73
 
74
+ function unionOfMatchKeys(matchKeys: ReadonlyMap<ConfigurableEditorAction, ReadonlySet<string>>): Set<string> {
75
+ const union = new Set<string>();
76
+ for (const keys of matchKeys.values()) {
77
+ for (const key of keys) union.add(key);
78
+ }
79
+ return union;
80
+ }
81
+
74
82
  const BRACKETED_PASTE_START = "\x1b[200~";
75
83
  const BRACKETED_PASTE_END = "\x1b[201~";
76
84
  const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
@@ -419,6 +427,17 @@ export class CustomEditor extends Editor {
419
427
  this.pendingImageLinks = [];
420
428
  }
421
429
 
430
+ /** Replace the composer draft with a restored historical prompt: sets the text and
431
+ * re-attaches the message's images so positional `[Image #N]` markers resolve on
432
+ * resubmit instead of degrading to literal text (esc-esc branch, `/tree`). Source
433
+ * links are unknown for restored drafts, so every link slot is `undefined`. */
434
+ setDraft(text: string, images?: readonly ImageContent[]): void {
435
+ this.setText(text);
436
+ this.imageLinks = undefined;
437
+ this.pendingImages = images ? [...images] : [];
438
+ this.pendingImageLinks = images ? images.map(() => undefined) : [];
439
+ }
440
+
422
441
  /** Treat image/paste markers as indivisible: a stray backspace deletes the whole token
423
442
  * instead of corrupting `[Paste #1, +30 lines]` into plain text. */
424
443
  override atomicTokenPattern = PLACEHOLDER_REGEX;
@@ -600,14 +619,14 @@ export class CustomEditor extends Editor {
600
619
  buildMatchKeys(keys),
601
620
  ]),
602
621
  );
622
+ /** Union of every action's match keys: one probe in `handleInput` decides
623
+ * whether the per-action interception chain can match at all. */
624
+ #actionMatchKeyUnion = unionOfMatchKeys(this.#actionMatchKeys);
603
625
 
604
626
  setActionKeys(action: ConfigurableEditorAction, keys: KeyId[]): void {
605
627
  this.#actionKeys.set(action, [...keys]);
606
- this.#rebuildActionMatchKeys(action);
607
- }
608
-
609
- #rebuildActionMatchKeys(action: ConfigurableEditorAction): void {
610
- this.#actionMatchKeys.set(action, buildMatchKeys(this.#actionKeys.get(action) ?? []));
628
+ this.#actionMatchKeys.set(action, buildMatchKeys(keys));
629
+ this.#actionMatchKeyUnion = unionOfMatchKeys(this.#actionMatchKeys);
611
630
  }
612
631
 
613
632
  #rebuildCustomMatchKeys(): void {
@@ -760,8 +779,10 @@ export class CustomEditor extends Editor {
760
779
  this.#pendingInput.push(data);
761
780
  return;
762
781
  }
763
- const hadBareQueuePrefix = this.getText() === "->" || this.getText() === "=>";
764
- const kittyParsed = parseKittySequence(data);
782
+ // textEquals avoids getText()'s O(buffer) join on every keystroke; kitty
783
+ // sequences always start with ESC, so plain bytes skip the native parse.
784
+ const hadBareQueuePrefix = this.textEquals("->") || this.textEquals("=>");
785
+ const kittyParsed = data.charCodeAt(0) === 0x1b ? parseKittySequence(data) : null;
765
786
  if (kittyParsed && (kittyParsed.modifier & 64) !== 0 && this.onCapsLock) {
766
787
  // Caps Lock is modifier bit 64
767
788
  this.onCapsLock();
@@ -823,7 +844,12 @@ export class CustomEditor extends Editor {
823
844
  // Space-hold push-to-talk: a sustained space bar starts/stops STT instead of typing spaces.
824
845
  if (this.#handleSpaceHold(data, canonical)) return;
825
846
 
826
- if (canonical !== undefined) {
847
+ // One union probe decides whether any per-action interception below can
848
+ // match — plain typing then skips the ~20 per-action set lookups per key.
849
+ if (
850
+ canonical !== undefined &&
851
+ (this.#actionMatchKeyUnion.has(canonical) || this.#customMatchKeys.has(canonical))
852
+ ) {
827
853
  // Intercept configured image paste (async - fires and handles result)
828
854
  if (this.#matchesAction(canonical, "app.clipboard.pasteImage") && this.onPasteImage) {
829
855
  void this.onPasteImage();
@@ -964,14 +990,11 @@ export class CustomEditor extends Editor {
964
990
 
965
991
  // Pass to parent for normal handling
966
992
  super.handleInput(data);
967
- const cursor = this.getCursor();
968
- if (
969
- !hadBareQueuePrefix &&
970
- (this.getText() === "->" || this.getText() === "=>") &&
971
- cursor.line === 0 &&
972
- cursor.col === 2
973
- ) {
974
- this.insertText("\n");
993
+ if (!hadBareQueuePrefix && (this.textEquals("->") || this.textEquals("=>"))) {
994
+ const cursor = this.getCursor();
995
+ if (cursor.line === 0 && cursor.col === 2) {
996
+ this.insertText("\n");
997
+ }
975
998
  }
976
999
  }
977
1000
 
@@ -433,11 +433,12 @@ const costSegment: StatusLineSegment = {
433
433
  id: "cost",
434
434
  render(ctx) {
435
435
  const { cost, premiumRequests } = ctx.usageStats;
436
+ const advisorCost = ctx.session.getAdvisorCost?.() ?? 0;
436
437
  const normalizedPremiumRequests = normalizePremiumRequests(premiumRequests);
437
438
  const state = ctx.session.state;
438
439
  const usingSubscription = state.model ? ctx.session.modelRegistry.isUsingOAuth(state.model) : false;
439
440
 
440
- if (!cost && !usingSubscription && !normalizedPremiumRequests) {
441
+ if (!cost && !advisorCost && !usingSubscription && !normalizedPremiumRequests) {
441
442
  return { content: "", visible: false };
442
443
  }
443
444
 
@@ -445,6 +446,7 @@ const costSegment: StatusLineSegment = {
445
446
  if (cost) billingParts.push(`$${cost.toFixed(2)}`);
446
447
  if (normalizedPremiumRequests) billingParts.push(`★ ${formatNumber(normalizedPremiumRequests)}`);
447
448
  if (usingSubscription) billingParts.push("(sub)");
449
+ if (advisorCost) billingParts.push(`${billingParts.length ? "+ " : ""}$${advisorCost.toFixed(2)} (adv)`);
448
450
 
449
451
  return { content: theme.fg("statusLineCost", billingParts.join(" ")), visible: true };
450
452
  },
@@ -22,4 +22,5 @@ Press ← ← to drill into a running or finished agent and inspect its tool cal
22
22
  Hit a Codex rate limit? `/usage reset` spends a saved reset credit to immediately restore your quota
23
23
  No native tool_calling? Inference provider botches parsing them? `PI_DIALECT=glm|kimi|anthropic…` rolls it locally for them!
24
24
  Turn on `/advisor` to attach a second model that reviews every turn and quietly injects advice
25
- Try starting your prompt with a ->, and writing a list (1. Do X, 2. Do Y)
25
+ Try starting your prompt with a ->, and writing a list (1. Do X, 2. Do Y)
26
+ Press shift+tab to cycle through reasoning effort levels
@@ -25,6 +25,7 @@ import { isWaitingPollDetails } from "../../tools/hub";
25
25
  import { formatStatusIcon, replaceTabs, resolveImageOptions } from "../../tools/render-utils";
26
26
  import { type FirstResultViewportRepaint, toolRenderers } from "../../tools/renderers";
27
27
  import { TODO_STRIKE_TOTAL_FRAMES, type TodoToolDetails } from "../../tools/todo";
28
+ import type { XdevState } from "../../tools/xdev";
28
29
  import { isFramedBlockComponent, markFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
29
30
  import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
30
31
  import { renderDiff } from "./diff";
@@ -1316,13 +1317,13 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
1316
1317
  }
1317
1318
  context.renderDiff = renderDiff;
1318
1319
  } else if (this.#toolName === "write") {
1319
- // Device-dispatch previews delegate to the mounted tool's own renderer;
1320
- // expose the session's xd:// registry so custom/MCP renderers survive dispatch.
1321
- const writeTool = this.#tool as
1322
- | { session?: { xdevRegistry?: { get(name: string): AgentTool | undefined } } }
1323
- | undefined;
1324
- const registry = writeTool?.session?.xdevRegistry;
1325
- if (registry) context.resolveXdevMounted = (name: string) => registry.get(name);
1320
+ // Device-dispatch previews resolve renderers from the canonical tool map.
1321
+ const writeTool = this.#tool as { session?: { xdev?: XdevState } } | undefined;
1322
+ const xdev = writeTool?.session?.xdev;
1323
+ if (xdev) {
1324
+ context.resolveXdevMounted = (name: string) =>
1325
+ xdev.mountedNames.has(name) ? xdev.tools.get(name) : undefined;
1326
+ }
1326
1327
  }
1327
1328
 
1328
1329
  return context;
@@ -346,7 +346,7 @@ export class CommandController {
346
346
  }
347
347
  }
348
348
 
349
- this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
349
+ this.ctx.presentCommandOutput([new Spacer(1), new Text(info, 1, 0)]);
350
350
  }
351
351
 
352
352
  static readonly #advisorStatusGlyph: Record<string, string> = {
@@ -368,7 +368,7 @@ export class CommandController {
368
368
  async handleAdvisorStatusCommand(): Promise<void> {
369
369
  const stats = this.ctx.session.getAdvisorStats();
370
370
  if (!stats.configured) {
371
- this.ctx.present([new Spacer(1), new Text("Advisor is disabled.", 1, 0)]);
371
+ this.ctx.presentCommandOutput([new Spacer(1), new Text("Advisor is disabled.", 1, 0)]);
372
372
  return;
373
373
  }
374
374
  // Fetch live quota data (cached 5 min by the auth-gateway) so we can show
@@ -434,7 +434,7 @@ export class CommandController {
434
434
  info += `${theme.fg("dim", "Tokens:")} ${stats.tokens.total.toLocaleString()}\n`;
435
435
  if (stats.cost > 0) info += `${theme.fg("dim", "Cost:")} $${stats.cost.toFixed(4)}\n`;
436
436
  }
437
- this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
437
+ this.ctx.presentCommandOutput([new Spacer(1), new Text(info, 1, 0)]);
438
438
  return;
439
439
  }
440
440
  // Single active advisor — detailed view.
@@ -480,7 +480,7 @@ export class CommandController {
480
480
  info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
481
481
  }
482
482
  if (stats.cost > 0) info += `${theme.fg("dim", "Cost:")} $${stats.cost.toFixed(4)}\n`;
483
- this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
483
+ this.ctx.presentCommandOutput([new Spacer(1), new Text(info, 1, 0)]);
484
484
  }
485
485
 
486
486
  async handleJobsCommand(): Promise<void> {
@@ -497,7 +497,7 @@ export class CommandController {
497
497
 
498
498
  if (snapshot.running.length === 0 && snapshot.recent.length === 0) {
499
499
  info += `\n${theme.fg("dim", "No async jobs yet.")}\n`;
500
- this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
500
+ this.ctx.presentCommandOutput([new Spacer(1), new Text(info, 1, 0)]);
501
501
  return;
502
502
  }
503
503
 
@@ -517,7 +517,7 @@ export class CommandController {
517
517
  }
518
518
  }
519
519
 
520
- this.ctx.present([new Spacer(1), new Text(info.trimEnd(), 1, 0)]);
520
+ this.ctx.presentCommandOutput([new Spacer(1), new Text(info.trimEnd(), 1, 0)]);
521
521
  }
522
522
 
523
523
  async handleUsageCommand(reports?: UsageReport[] | null): Promise<void> {
@@ -558,7 +558,7 @@ export class CommandController {
558
558
  provider => (provider === currentProvider ? activeAccount : undefined),
559
559
  usageModelSelectors,
560
560
  );
561
- this.ctx.present([new Spacer(1), new Text(output, 1, 0)]);
561
+ this.ctx.presentCommandOutput([new Spacer(1), new Text(output, 1, 0)]);
562
562
  }
563
563
 
564
564
  async handleChangelogCommand(showFull = false): Promise<void> {
@@ -578,7 +578,7 @@ export class CommandController {
578
578
  block.addChild(new Spacer(1));
579
579
  block.addChild(new Markdown(changelogMarkdown + hint, 1, 1, getMarkdownTheme()));
580
580
  block.addChild(new DynamicBorder());
581
- this.ctx.present(block);
581
+ this.ctx.presentCommandOutput(block);
582
582
  }
583
583
 
584
584
  handleHotkeysCommand(): void {
@@ -607,7 +607,7 @@ export class CommandController {
607
607
  block.addChild(new Spacer(1));
608
608
  block.addChild(new Text(output, 1, 0));
609
609
  block.addChild(new DynamicBorder());
610
- this.ctx.present(block);
610
+ this.ctx.presentCommandOutput(block);
611
611
  }
612
612
 
613
613
  async handleMemoryCommand(text: string): Promise<void> {
@@ -628,7 +628,7 @@ export class CommandController {
628
628
  block.addChild(new Spacer(1));
629
629
  block.addChild(new Markdown(payload, 1, 1, getMarkdownTheme()));
630
630
  block.addChild(new DynamicBorder());
631
- this.ctx.present(block);
631
+ this.ctx.presentCommandOutput(block);
632
632
  return;
633
633
  }
634
634
 
@@ -86,7 +86,7 @@ export class ExtensionUiController {
86
86
  const uiContext: ExtensionUIContext = {
87
87
  timeoutStartsOnPresentation: true,
88
88
  select: (title, options, dialogOptions) => this.showCollabAwareSelector(title, options, dialogOptions),
89
- confirm: (title, message, _dialogOptions) => this.showHookConfirm(title, message),
89
+ confirm: (title, message, dialogOptions) => this.showHookConfirm(title, message, dialogOptions),
90
90
  input: (title, placeholder, dialogOptions) => this.showHookInput(title, placeholder, dialogOptions),
91
91
  askDialog: (questions, dialogOptions) => this.showAskDialog(questions, dialogOptions),
92
92
  notify: (message, type) => this.showHookNotify(message, type),
@@ -246,7 +246,7 @@ export class ExtensionUiController {
246
246
  // Update UI
247
247
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
248
248
  await this.ctx.reloadTodos();
249
- this.ctx.editor.setText(result.selectedText);
249
+ this.ctx.editor.setDraft(result.selectedText, result.selectedImages);
250
250
  this.ctx.showStatus("Branched to new session");
251
251
 
252
252
  return { cancelled: false };
@@ -261,7 +261,7 @@ export class ExtensionUiController {
261
261
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
262
262
  await this.ctx.reloadTodos();
263
263
  if (result.editorText && !this.ctx.editor.getText().trim()) {
264
- this.ctx.editor.setText(result.editorText);
264
+ this.ctx.editor.setDraft(result.editorText, result.editorImages);
265
265
  }
266
266
  this.ctx.showStatus("Navigated to selected point");
267
267
 
@@ -476,7 +476,7 @@ export class ExtensionUiController {
476
476
  // Update UI
477
477
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
478
478
  await this.ctx.reloadTodos();
479
- this.ctx.editor.setText(result.selectedText);
479
+ this.ctx.editor.setDraft(result.selectedText, result.selectedImages);
480
480
  this.ctx.showStatus("Branched to new session");
481
481
 
482
482
  return { cancelled: false };
@@ -491,7 +491,7 @@ export class ExtensionUiController {
491
491
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
492
492
  await this.ctx.reloadTodos();
493
493
  if (result.editorText && !this.ctx.editor.getText().trim()) {
494
- this.ctx.editor.setText(result.editorText);
494
+ this.ctx.editor.setDraft(result.editorText, result.editorImages);
495
495
  }
496
496
  this.ctx.showStatus("Navigated to selected point");
497
497
 
@@ -941,8 +941,8 @@ export class ExtensionUiController {
941
941
  /**
942
942
  * Show a confirmation dialog for hooks.
943
943
  */
944
- async showHookConfirm(title: string, message: string): Promise<boolean> {
945
- const result = await this.showHookSelector(`${title}\n${message}`, ["Yes", "No"]);
944
+ async showHookConfirm(title: string, message: string, dialogOptions?: ExtensionUIDialogOptions): Promise<boolean> {
945
+ const result = await this.showHookSelector(`${title}\n${message}`, ["Yes", "No"], dialogOptions);
946
946
  return result === "Yes";
947
947
  }
948
948
 
@@ -455,6 +455,11 @@ export class SelectorController {
455
455
  this.ctx.showError(`Failed to apply memory backend: ${err}`);
456
456
  });
457
457
  break;
458
+ case "inspect_image.mode":
459
+ void this.ctx.session.applyInspectImageModeChange().catch(err => {
460
+ this.ctx.showError(`Failed to apply vision mode: ${err}`);
461
+ });
462
+ break;
458
463
 
459
464
  case "autocompleteMaxVisible":
460
465
  this.ctx.editor.setAutocompleteMaxVisible(typeof value === "number" ? value : Number(value));
@@ -1097,7 +1102,7 @@ export class SelectorController {
1097
1102
  }
1098
1103
 
1099
1104
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
1100
- this.ctx.editor.setText(result.selectedText);
1105
+ this.ctx.editor.setDraft(result.selectedText, result.selectedImages);
1101
1106
  done();
1102
1107
  this.ctx.showStatus("Branched to new session");
1103
1108
  },
@@ -1272,7 +1277,7 @@ export class SelectorController {
1272
1277
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
1273
1278
  await this.ctx.reloadTodos();
1274
1279
  if (result.editorText && !this.ctx.editor.getText().trim()) {
1275
- this.ctx.editor.setText(result.editorText);
1280
+ this.ctx.editor.setDraft(result.editorText, result.editorImages);
1276
1281
  }
1277
1282
  this.ctx.showStatus("Navigated to selected point");
1278
1283