@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.0

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 (285) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +1 -1
  3. package/dist/cli.js +4659 -4261
  4. package/dist/types/async/job-manager.d.ts +37 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/bench-cli.d.ts +41 -0
  7. package/dist/types/commands/bench.d.ts +15 -3
  8. package/dist/types/commands/launch.d.ts +4 -0
  9. package/dist/types/config/model-registry.d.ts +3 -1
  10. package/dist/types/config/models-config-schema.d.ts +18 -6
  11. package/dist/types/config/models-config.d.ts +14 -5
  12. package/dist/types/config/provider-globals.d.ts +2 -2
  13. package/dist/types/config/service-tier.d.ts +5 -1
  14. package/dist/types/config/settings-schema.d.ts +316 -47
  15. package/dist/types/config/settings.d.ts +7 -0
  16. package/dist/types/dap/client.d.ts +20 -0
  17. package/dist/types/exec/bash-executor.d.ts +38 -0
  18. package/dist/types/exec/direnv.d.ts +33 -0
  19. package/dist/types/export/html/args.d.ts +15 -0
  20. package/dist/types/export/html/index.d.ts +11 -26
  21. package/dist/types/export/html/web-palette.d.ts +55 -116
  22. package/dist/types/extensibility/extensions/loader.d.ts +3 -0
  23. package/dist/types/extensibility/extensions/types.d.ts +17 -2
  24. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
  25. package/dist/types/live/controller.d.ts +40 -0
  26. package/dist/types/live/protocol.d.ts +85 -0
  27. package/dist/types/live/protocol.test.d.ts +1 -0
  28. package/dist/types/live/transport.d.ts +48 -0
  29. package/dist/types/live/visualizer.d.ts +33 -0
  30. package/dist/types/lsp/client.d.ts +16 -1
  31. package/dist/types/mcp/config-writer.test.d.ts +1 -0
  32. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  33. package/dist/types/mcp/smithery-auth.d.ts +1 -1
  34. package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
  35. package/dist/types/memory-backend/tool-names.d.ts +2 -0
  36. package/dist/types/modes/components/login-dialog.d.ts +2 -2
  37. package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
  38. package/dist/types/modes/components/settings-defs.d.ts +7 -1
  39. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +9 -1
  41. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/event-controller.d.ts +6 -1
  43. package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
  44. package/dist/types/modes/interactive-mode.d.ts +5 -0
  45. package/dist/types/modes/theme/theme.d.ts +1 -1
  46. package/dist/types/modes/types.d.ts +3 -1
  47. package/dist/types/modes/utils/context-usage.d.ts +12 -2
  48. package/dist/types/plan-mode/plan-files.d.ts +10 -0
  49. package/dist/types/registry/agent-lifecycle.d.ts +21 -9
  50. package/dist/types/registry/agent-registry.d.ts +11 -4
  51. package/dist/types/sdk.d.ts +15 -1
  52. package/dist/types/secrets/index.d.ts +15 -2
  53. package/dist/types/secrets/obfuscator.d.ts +113 -15
  54. package/dist/types/session/acp-permission-gate.d.ts +19 -0
  55. package/dist/types/session/agent-session-events.d.ts +84 -0
  56. package/dist/types/session/agent-session-types.d.ts +314 -0
  57. package/dist/types/session/agent-session.d.ts +118 -755
  58. package/dist/types/session/async-job-delivery.d.ts +28 -0
  59. package/dist/types/session/bash-runner.d.ts +75 -0
  60. package/dist/types/session/blob-store.d.ts +11 -1
  61. package/dist/types/session/blob-store.test.d.ts +1 -0
  62. package/dist/types/session/checkpoint-entries.d.ts +28 -0
  63. package/dist/types/session/eval-runner.d.ts +49 -0
  64. package/dist/types/session/irc-bridge.d.ts +46 -0
  65. package/dist/types/session/messages.d.ts +22 -0
  66. package/dist/types/session/model-controls.d.ts +180 -0
  67. package/dist/types/session/prewalk.d.ts +46 -0
  68. package/dist/types/session/queued-messages.d.ts +22 -0
  69. package/dist/types/session/redis-session-storage.d.ts +1 -0
  70. package/dist/types/session/retry-fallback-chains.d.ts +74 -0
  71. package/dist/types/session/role-models.d.ts +15 -0
  72. package/dist/types/session/session-advisors.d.ts +228 -0
  73. package/dist/types/session/session-entries.d.ts +8 -0
  74. package/dist/types/session/session-handoff.d.ts +70 -0
  75. package/dist/types/session/session-history-format.d.ts +6 -1
  76. package/dist/types/session/session-maintenance.d.ts +246 -0
  77. package/dist/types/session/session-manager.d.ts +41 -0
  78. package/dist/types/session/session-memory.d.ts +56 -0
  79. package/dist/types/session/session-provider-boundary.d.ts +62 -0
  80. package/dist/types/session/session-stats.d.ts +48 -0
  81. package/dist/types/session/session-tools.d.ts +143 -0
  82. package/dist/types/session/session-workspace.d.ts +32 -0
  83. package/dist/types/session/stream-guards.d.ts +44 -0
  84. package/dist/types/session/todo-tracker.d.ts +56 -0
  85. package/dist/types/session/ttsr-coordinator.d.ts +51 -0
  86. package/dist/types/session/turn-recovery.d.ts +225 -0
  87. package/dist/types/system-prompt.d.ts +2 -0
  88. package/dist/types/task/executor.d.ts +4 -1
  89. package/dist/types/task/output-manager.d.ts +3 -1
  90. package/dist/types/tools/approval.d.ts +1 -0
  91. package/dist/types/tools/browser/launch.d.ts +5 -0
  92. package/dist/types/tools/image-gen.d.ts +4 -4
  93. package/dist/types/tools/image-providers.d.ts +38 -0
  94. package/dist/types/tools/index.d.ts +5 -0
  95. package/dist/types/tools/path-utils.d.ts +8 -0
  96. package/dist/types/tools/todo.d.ts +10 -4
  97. package/dist/types/tools/xdev.d.ts +5 -1
  98. package/dist/types/tts/streaming-player.d.ts +14 -0
  99. package/dist/types/tts/vocalizer.d.ts +5 -0
  100. package/dist/types/utils/jj.d.ts +29 -0
  101. package/dist/types/utils/lang-from-path.d.ts +1 -0
  102. package/dist/types/utils/title-generator.d.ts +30 -0
  103. package/dist/types/vibe/runtime.d.ts +36 -8
  104. package/dist/types/web/search/index.d.ts +1 -1
  105. package/dist/types/web/search/provider.d.ts +15 -5
  106. package/dist/types/web/search/types.d.ts +94 -0
  107. package/package.json +12 -12
  108. package/scripts/generate-share-viewer.ts +4 -6
  109. package/src/advisor/__tests__/advisor.test.ts +643 -0
  110. package/src/advisor/runtime.ts +144 -43
  111. package/src/async/job-manager.ts +90 -2
  112. package/src/cli/args.ts +2 -0
  113. package/src/cli/auth-gateway-cli.ts +18 -3
  114. package/src/cli/bench-cli.ts +395 -29
  115. package/src/cli/flag-tables.ts +3 -0
  116. package/src/cli/gc-cli.ts +2 -2
  117. package/src/cli/usage-cli.ts +10 -1
  118. package/src/collab/host.ts +3 -2
  119. package/src/commands/bench.ts +18 -4
  120. package/src/commands/launch.ts +4 -0
  121. package/src/config/model-registry.ts +10 -2
  122. package/src/config/models-config-schema.ts +15 -4
  123. package/src/config/provider-globals.ts +9 -9
  124. package/src/config/service-tier.ts +26 -1
  125. package/src/config/settings-schema.ts +209 -51
  126. package/src/config/settings.ts +81 -2
  127. package/src/dap/client.ts +37 -4
  128. package/src/edit/index.ts +49 -12
  129. package/src/eval/py/executor.ts +103 -33
  130. package/src/exec/bash-executor.ts +89 -6
  131. package/src/exec/direnv.ts +145 -0
  132. package/src/export/html/args.ts +20 -0
  133. package/src/export/html/index.ts +45 -47
  134. package/src/export/html/template.css +19 -1
  135. package/src/export/html/template.html +6 -0
  136. package/src/export/html/template.js +21 -0
  137. package/src/export/html/tool-views.generated.js +31 -31
  138. package/src/export/html/web-palette.ts +116 -132
  139. package/src/export/share.ts +249 -49
  140. package/src/extensibility/extensions/loader.ts +29 -1
  141. package/src/extensibility/extensions/runner.ts +6 -0
  142. package/src/extensibility/extensions/types.ts +33 -2
  143. package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
  144. package/src/hindsight/state.ts +64 -6
  145. package/src/internal-urls/local-protocol.ts +2 -1
  146. package/src/internal-urls/memory-protocol.ts +2 -2
  147. package/src/internal-urls/skill-protocol.ts +2 -2
  148. package/src/internal-urls/ssh-protocol.ts +2 -1
  149. package/src/internal-urls/vault-protocol.ts +2 -1
  150. package/src/live/audio-worklet.txt +59 -0
  151. package/src/live/browser-runtime.txt +221 -0
  152. package/src/live/controller.ts +464 -0
  153. package/src/live/prompts/agent-final-message.md +3 -0
  154. package/src/live/prompts/live-instructions.md +23 -0
  155. package/src/live/protocol.test.ts +140 -0
  156. package/src/live/protocol.ts +233 -0
  157. package/src/live/transport.ts +490 -0
  158. package/src/live/visualizer.ts +234 -0
  159. package/src/lsp/client.ts +29 -9
  160. package/src/lsp/index.ts +19 -4
  161. package/src/main.ts +8 -1
  162. package/src/mcp/config-writer.test.ts +43 -0
  163. package/src/mcp/config-writer.ts +109 -82
  164. package/src/mcp/oauth-discovery.ts +10 -2
  165. package/src/mcp/smithery-auth.test.ts +29 -0
  166. package/src/mcp/smithery-auth.ts +3 -2
  167. package/src/memories/index.ts +40 -20
  168. package/src/memory-backend/tool-names.ts +2 -0
  169. package/src/mnemopi/backend.ts +24 -7
  170. package/src/modes/acp/acp-agent.ts +56 -10
  171. package/src/modes/acp/acp-event-mapper.ts +8 -1
  172. package/src/modes/components/agent-hub.ts +1 -1
  173. package/src/modes/components/login-dialog.ts +14 -4
  174. package/src/modes/components/plan-review-overlay.ts +350 -35
  175. package/src/modes/components/settings-defs.ts +23 -1
  176. package/src/modes/components/settings-selector.ts +169 -1
  177. package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
  178. package/src/modes/components/status-line/component.ts +123 -2
  179. package/src/modes/components/tool-execution.ts +11 -2
  180. package/src/modes/controllers/command-controller.ts +56 -15
  181. package/src/modes/controllers/event-controller.ts +111 -16
  182. package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
  183. package/src/modes/controllers/extension-ui-controller.ts +20 -4
  184. package/src/modes/controllers/live-command-controller.ts +178 -0
  185. package/src/modes/controllers/mcp-command-controller.ts +10 -1
  186. package/src/modes/controllers/omfg-controller.ts +44 -40
  187. package/src/modes/controllers/selector-controller.ts +18 -10
  188. package/src/modes/controllers/session-focus-controller.ts +6 -1
  189. package/src/modes/interactive-mode.ts +157 -38
  190. package/src/modes/runtime-init.ts +2 -0
  191. package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
  192. package/src/modes/theme/theme.ts +1 -1
  193. package/src/modes/types.ts +3 -1
  194. package/src/modes/utils/context-usage.ts +23 -7
  195. package/src/modes/utils/ui-helpers.ts +10 -5
  196. package/src/plan-mode/plan-files.ts +40 -0
  197. package/src/prompts/bench/cache-prefix-chunk.md +1 -0
  198. package/src/prompts/bench/cache-prefix.md +3 -0
  199. package/src/prompts/bench/cache-suffix.md +1 -0
  200. package/src/prompts/system/custom-system-prompt.md +1 -1
  201. package/src/prompts/system/project-prompt.md +8 -1
  202. package/src/prompts/system/subagent-async-pending.md +6 -0
  203. package/src/prompts/system/system-prompt.md +1 -1
  204. package/src/prompts/system/workflow-notice.md +48 -40
  205. package/src/prompts/system/xdev-mount-notice.md +4 -0
  206. package/src/prompts/tools/todo.md +3 -1
  207. package/src/registry/agent-lifecycle.ts +116 -46
  208. package/src/registry/agent-registry.ts +35 -8
  209. package/src/registry/persisted-agents.ts +26 -2
  210. package/src/sdk.ts +210 -136
  211. package/src/secrets/index.ts +127 -4
  212. package/src/secrets/obfuscator.ts +2328 -105
  213. package/src/session/acp-permission-gate.ts +165 -0
  214. package/src/session/agent-session-error-log.test.ts +1 -1
  215. package/src/session/agent-session-events.ts +66 -0
  216. package/src/session/agent-session-types.ts +334 -0
  217. package/src/session/agent-session.ts +1892 -12226
  218. package/src/session/artifacts.ts +6 -5
  219. package/src/session/async-job-delivery.ts +74 -0
  220. package/src/session/bash-runner.ts +326 -0
  221. package/src/session/blob-store.test.ts +56 -0
  222. package/src/session/blob-store.ts +18 -2
  223. package/src/session/checkpoint-entries.ts +81 -0
  224. package/src/session/eval-runner.ts +212 -0
  225. package/src/session/indexed-session-storage.ts +9 -2
  226. package/src/session/irc-bridge.ts +203 -0
  227. package/src/session/messages.ts +263 -1
  228. package/src/session/model-controls.ts +714 -0
  229. package/src/session/prewalk.ts +252 -0
  230. package/src/session/queued-messages.ts +93 -0
  231. package/src/session/redis-session-storage.ts +51 -11
  232. package/src/session/retry-fallback-chains.ts +455 -0
  233. package/src/session/role-models.ts +85 -0
  234. package/src/session/session-advisors.ts +1679 -0
  235. package/src/session/session-context.ts +6 -3
  236. package/src/session/session-entries.ts +8 -0
  237. package/src/session/session-handoff.ts +308 -0
  238. package/src/session/session-history-format.ts +20 -9
  239. package/src/session/session-loader.ts +0 -46
  240. package/src/session/session-maintenance.ts +2983 -0
  241. package/src/session/session-manager.ts +390 -30
  242. package/src/session/session-memory.ts +222 -0
  243. package/src/session/session-provider-boundary.ts +307 -0
  244. package/src/session/session-stats.ts +293 -0
  245. package/src/session/session-tools.ts +943 -0
  246. package/src/session/session-workspace.ts +53 -0
  247. package/src/session/stream-guards.ts +417 -0
  248. package/src/session/todo-tracker.ts +380 -0
  249. package/src/session/ttsr-coordinator.ts +496 -0
  250. package/src/session/turn-recovery.ts +1629 -0
  251. package/src/slash-commands/builtin-registry.ts +92 -11
  252. package/src/slash-commands/helpers/usage-report.ts +22 -3
  253. package/src/system-prompt.ts +41 -17
  254. package/src/task/executor.ts +285 -70
  255. package/src/task/index.ts +98 -105
  256. package/src/task/output-manager.ts +25 -3
  257. package/src/task/persisted-revive.ts +4 -3
  258. package/src/task/structured-subagent.ts +1 -0
  259. package/src/tools/approval.ts +55 -10
  260. package/src/tools/bash-interactive.ts +26 -0
  261. package/src/tools/bash-skill-urls.ts +28 -2
  262. package/src/tools/bash.ts +339 -123
  263. package/src/tools/browser/launch.ts +9 -1
  264. package/src/tools/browser/tab-supervisor.ts +8 -5
  265. package/src/tools/browser.ts +94 -47
  266. package/src/tools/image-gen.ts +20 -28
  267. package/src/tools/image-providers.ts +50 -0
  268. package/src/tools/index.ts +9 -0
  269. package/src/tools/path-utils.ts +49 -2
  270. package/src/tools/read.ts +156 -88
  271. package/src/tools/renderers.ts +7 -1
  272. package/src/tools/todo.ts +101 -11
  273. package/src/tools/vibe.ts +1 -2
  274. package/src/tools/write.ts +51 -1
  275. package/src/tools/xdev.ts +67 -3
  276. package/src/tts/streaming-player.ts +62 -4
  277. package/src/tts/vocalizer.ts +18 -1
  278. package/src/utils/jj.ts +125 -4
  279. package/src/utils/lang-from-path.ts +7 -0
  280. package/src/utils/title-generator.ts +137 -1
  281. package/src/vibe/runtime.ts +852 -72
  282. package/src/web/search/index.ts +3 -6
  283. package/src/web/search/provider.ts +31 -16
  284. package/src/web/search/providers/xai.ts +7 -1
  285. package/src/web/search/types.ts +3 -0
@@ -10,6 +10,7 @@ import { Settings, type ShellMinimizerSettings } from "../config/settings";
10
10
  import { OutputSink } from "../session/streaming-output";
11
11
  import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
12
12
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
13
+ import { loadDirenvEnv } from "./direnv";
13
14
  import { buildNonInteractiveEnv } from "./non-interactive-env";
14
15
 
15
16
  export interface BashExecutorOptions {
@@ -56,6 +57,78 @@ export interface BashResult {
56
57
  workingDir?: string;
57
58
  }
58
59
 
60
+ /** POSIX-safe variable name — gates which direnv unsets we inject into the
61
+ * command line, so a hostile `.envrc` can't smuggle shell syntax through
62
+ * `unset`. `.envrc` never produces non-identifier names in practice. */
63
+ const SAFE_ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
64
+
65
+ export interface DirenvPreflightOptions {
66
+ /** Caller-supplied env overlay; these values win over direnv-provided ones. */
67
+ callerEnv?: Record<string, string>;
68
+ signal?: AbortSignal;
69
+ /** Full direnv-load budget (`bash.direnvLoadTimeoutMs`). A positive
70
+ * `callerTimeoutMs` clamps the effective load below this; `0`/undefined
71
+ * leaves the full budget. */
72
+ timeoutMs?: number;
73
+ /** The caller's command deadline (ms). A positive value clamps the direnv
74
+ * load so a cold `.envrc` can't outlast a short-timeout command; `0` or
75
+ * undefined means "no caller clamp" — the load keeps its full `timeoutMs`
76
+ * budget (a disabled command deadline is NOT a 0 ms load). Centralizing the
77
+ * clamp here keeps every backend (executeBash, ACP terminal, PTY) on one
78
+ * contract instead of each re-deriving it. */
79
+ callerTimeoutMs?: number;
80
+ /** `bash.direnv` setting — `"off"` skips the load entirely. */
81
+ direnvSetting: "auto" | "off";
82
+ /** Shell wrapper prefix (profiler/strace) to place *after* the unset prefix,
83
+ * matching `executeBash`'s ordering. Backends that apply their own shell
84
+ * wrapping (ACP `wrapShellLineForClientTerminal`) omit this. */
85
+ commandPrefix?: string | undefined;
86
+ }
87
+
88
+ /**
89
+ * Load the repo's direnv/devenv env and fold it into a `(command, env)` pair so
90
+ * every bash backend (one-shot `executeBash`, ACP client terminal, PTY) exposes
91
+ * the same devenv tools. Encapsulates: load the diff, merge `set` under the
92
+ * caller's overlay (caller wins), and prepend a regex-gated `unset -v` for
93
+ * variables the `.envrc` removes (skipping any the caller re-supplied).
94
+ *
95
+ * Returns the possibly-prefixed command plus the merged env, or the inputs
96
+ * unchanged (`env` = `callerEnv`) when direnv is off, absent, or has no `.envrc`.
97
+ * Pure transform: does NOT layer non-interactive env defaults — that stays the
98
+ * caller's job (so interactive PTY/ACP paths keep their own env shape).
99
+ */
100
+ export async function applyDirenvPreflight(
101
+ command: string,
102
+ cwd: string,
103
+ opts: DirenvPreflightOptions,
104
+ ): Promise<{ command: string; env: Record<string, string> | undefined }> {
105
+ const withPrefix = (line: string): string => (opts.commandPrefix ? `${opts.commandPrefix} ${line}` : line);
106
+ // A positive caller deadline clamps the direnv load below its full budget so
107
+ // a cold `.envrc` can't outlast a short-timeout command; `0`/undefined means
108
+ // "no caller clamp" (a disabled command deadline is not a 0 ms load). Every
109
+ // backend routes through here, so the clamp lives in one place.
110
+ const loadTimeoutMs =
111
+ opts.callerTimeoutMs !== undefined && opts.callerTimeoutMs > 0
112
+ ? Math.min(opts.timeoutMs ?? opts.callerTimeoutMs, opts.callerTimeoutMs)
113
+ : opts.timeoutMs;
114
+ const direnvDiff =
115
+ opts.direnvSetting === "off" ? null : await loadDirenvEnv(cwd, { timeoutMs: loadTimeoutMs, signal: opts.signal });
116
+ if (!direnvDiff) {
117
+ return { command: withPrefix(command), env: opts.callerEnv };
118
+ }
119
+ // The caller's explicit env still wins over direnv-provided values.
120
+ const mergedEnv = { ...direnvDiff.set, ...opts.callerEnv };
121
+ // direnv can also *remove* inherited variables (a `.envrc` doing
122
+ // `unset AWS_PROFILE`). An env overlay can only add/override, so prepend a
123
+ // real `unset` for those — unless the caller re-supplied the same var
124
+ // explicitly, in which case the caller wins.
125
+ const direnvUnsets = direnvDiff.unset.filter(
126
+ name => !(opts.callerEnv && name in opts.callerEnv) && SAFE_ENV_NAME.test(name),
127
+ );
128
+ const unsetPrefix = direnvUnsets.length > 0 ? `unset -v ${direnvUnsets.join(" ")}; ` : "";
129
+ return { command: `${unsetPrefix}${withPrefix(command)}`, env: mergedEnv };
130
+ }
131
+
59
132
  const shellSessions = new Map<string, Shell>();
60
133
  const brokenShellSessions = new Set<string>();
61
134
  const shellSessionQuarantines = new Map<string, Promise<unknown>>();
@@ -279,15 +352,25 @@ export async function executeBash(command: string, options?: BashExecutorOptions
279
352
  const minimizer = buildMinimizerOptions(settings.getGroup("shellMinimizer"));
280
353
 
281
354
  const commandCwd = resolveShellCwd(options?.cwd);
282
- const commandEnv = buildNonInteractiveEnv(options?.env);
283
-
284
- // Apply command prefix if configured
285
- const prefixedCommand = prefix ? `${prefix} ${command}` : command;
355
+ // Fold the repo's direnv/devenv env into the command + env so devenv tools
356
+ // land on PATH; the caller's explicit `env` still wins. Thread the caller's
357
+ // signal + timeout so an aborted / short-timeout call can't hang on a cold
358
+ // `.envrc` load before the abort listener is installed. The helper applies
359
+ // the configured shell `prefix` after any `unset -v` it prepends.
360
+ const preflight = await applyDirenvPreflight(command, commandCwd ?? process.cwd(), {
361
+ callerEnv: options?.env,
362
+ signal: options?.signal,
363
+ timeoutMs: settings.get("bash.direnvLoadTimeoutMs"),
364
+ callerTimeoutMs: options?.timeout,
365
+ direnvSetting: settings.get("bash.direnv"),
366
+ commandPrefix: prefix,
367
+ });
368
+ const commandEnv = buildNonInteractiveEnv(preflight.env);
286
369
  const runCdInPersistentShell = options?.useUserShell === true && !prefix && isPersistentShellCdCommand(command);
287
370
  const finalCommand =
288
371
  options?.useUserShell === true && !bashShell && !runCdInPersistentShell
289
- ? buildUserShellCommand(shell, args, prefixedCommand)
290
- : prefixedCommand;
372
+ ? buildUserShellCommand(shell, args, preflight.command)
373
+ : preflight.command;
291
374
 
292
375
  // Create output sink for truncation and artifact handling
293
376
  const sink = new OutputSink({
@@ -0,0 +1,145 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { $which, logger } from "@oh-my-pi/pi-utils";
4
+
5
+ /** Default cap on a single `direnv` invocation. The first export for a devenv
6
+ * `.envrc` can build a shell; callers may raise this via `bash.direnvLoadTimeoutMs`. */
7
+ export const DEFAULT_DIRENV_TIMEOUT_MS = 30_000;
8
+
9
+ /** Walk up from `startDir` to the nearest directory containing an `.envrc`. */
10
+ export async function findEnvrc(startDir: string): Promise<string | null> {
11
+ let dir = path.resolve(startDir);
12
+ for (;;) {
13
+ const candidate = path.join(dir, ".envrc");
14
+ try {
15
+ if ((await fs.stat(candidate)).isFile()) return candidate;
16
+ } catch {
17
+ // no .envrc here — keep walking up
18
+ }
19
+ const parent = path.dirname(dir);
20
+ if (parent === dir) return null;
21
+ dir = parent;
22
+ }
23
+ }
24
+
25
+ export interface DirenvExportDiff {
26
+ /** Variables direnv sets to a concrete value. */
27
+ set: Record<string, string>;
28
+ /** Variables direnv removes (JSON `null`). */
29
+ unset: string[];
30
+ }
31
+
32
+ /** Parse `direnv export json` output (`{VAR: value|null}`) into set/unset halves. */
33
+ export function parseDirenvExport(jsonText: string): DirenvExportDiff {
34
+ const trimmed = jsonText.trim();
35
+ if (trimmed.length === 0) return { set: {}, unset: [] };
36
+ let parsed: unknown;
37
+ try {
38
+ parsed = JSON.parse(trimmed);
39
+ } catch {
40
+ return { set: {}, unset: [] };
41
+ }
42
+ const set: Record<string, string> = {};
43
+ const unset: string[] = [];
44
+ if (parsed && typeof parsed === "object") {
45
+ for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
46
+ if (value === null) unset.push(key);
47
+ else if (typeof value === "string") set[key] = value;
48
+ }
49
+ }
50
+ return { set, unset };
51
+ }
52
+
53
+ let direnvLookup: { bin: string | null } | undefined;
54
+ function direnvBinary(): string | null {
55
+ if (!direnvLookup) direnvLookup = { bin: $which("direnv") };
56
+ return direnvLookup.bin;
57
+ }
58
+
59
+ /** direnv computes its diff relative to the spawning env; strip any inherited
60
+ * direnv state so it loads the target `.envrc` from a clean baseline. */
61
+ function cleanSpawnEnv(): Record<string, string> {
62
+ const out: Record<string, string> = {};
63
+ for (const [key, value] of Object.entries(Bun.env)) {
64
+ if (value !== undefined && !key.startsWith("DIRENV_")) out[key] = value;
65
+ }
66
+ return out;
67
+ }
68
+
69
+ async function runDirenv(
70
+ bin: string,
71
+ args: string[],
72
+ cwd: string,
73
+ timeoutMs: number,
74
+ env: Record<string, string>,
75
+ signal?: AbortSignal,
76
+ ): Promise<{ exitCode: number; stdout: string; stderr: string }> {
77
+ // Bail on the caller's cancellation as well as the per-invocation cap so a
78
+ // cold `.envrc` load can't outlive an aborted / short-timeout bash call.
79
+ const abortSignal = signal
80
+ ? AbortSignal.any([signal, AbortSignal.timeout(timeoutMs)])
81
+ : AbortSignal.timeout(timeoutMs);
82
+ const proc = Bun.spawn([bin, ...args], {
83
+ cwd,
84
+ env,
85
+ stdout: "pipe",
86
+ stderr: "pipe",
87
+ signal: abortSignal,
88
+ });
89
+ // Drain stdout AND stderr concurrently: a cold `use devenv`/Nix load emits
90
+ // enough diagnostics to fill the stderr pipe and block the child forever if
91
+ // only stdout is read (it would then wait out `timeoutMs`).
92
+ const [stdout, stderr] = await Promise.all([
93
+ new Response(proc.stdout as ReadableStream<Uint8Array>).text(),
94
+ new Response(proc.stderr as ReadableStream<Uint8Array>).text(),
95
+ ]);
96
+ const exitCode = await proc.exited;
97
+ return { exitCode, stdout, stderr };
98
+ }
99
+
100
+ /**
101
+ * Resolve the nearest `.envrc` from `cwd` and return its `direnv export` diff
102
+ * (variables to set, and variables direnv removes). Returns `null` when there
103
+ * is no `.envrc`, `direnv` is not installed, the `.envrc` is not on direnv's
104
+ * allow list, or the export fails/times out.
105
+ *
106
+ * direnv's own allow list is honored — an `.envrc` the user has not
107
+ * `direnv allow`ed is NEVER executed or auto-allowed. This keeps OMP's trust
108
+ * boundary identical to the user's own shell: cloning a repo with a poisoned
109
+ * `.envrc` grants it nothing until the user explicitly allows it.
110
+ *
111
+ * Always re-invokes `direnv export json` rather than serving a cached diff:
112
+ * direnv is fast once warm, and its own watch/mtime invalidation is the
113
+ * authoritative freshness signal (a content-hash cache here would go stale when
114
+ * a `watch_file` target changes without the `.envrc` text changing).
115
+ */
116
+ export async function loadDirenvEnv(
117
+ cwd: string,
118
+ opts?: { timeoutMs?: number; signal?: AbortSignal },
119
+ ): Promise<DirenvExportDiff | null> {
120
+ const envrcPath = await findEnvrc(cwd);
121
+ if (!envrcPath) return null;
122
+ const bin = direnvBinary();
123
+ if (!bin) return null;
124
+
125
+ const dir = path.dirname(envrcPath);
126
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_DIRENV_TIMEOUT_MS;
127
+ const env = cleanSpawnEnv();
128
+ try {
129
+ const { exitCode, stdout, stderr } = await runDirenv(bin, ["export", "json"], dir, timeoutMs, env, opts?.signal);
130
+ if (exitCode !== 0) {
131
+ // A not-yet-allowed .envrc is an expected steady state (the user opted
132
+ // out by never running `direnv allow`), not an error worth warning on.
133
+ if (stderr.includes("is blocked")) {
134
+ logger.debug("direnv .envrc not allowed; skipping", { dir });
135
+ } else {
136
+ logger.warn("direnv export failed", { dir, exitCode });
137
+ }
138
+ return null;
139
+ }
140
+ return parseDirenvExport(stdout);
141
+ } catch (err) {
142
+ logger.warn("direnv load failed", { dir, error: err instanceof Error ? err.message : String(err) });
143
+ return null;
144
+ }
145
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `/export` argument parsing, split from `./index.ts` so slash-command
3
+ * registries can parse arguments without eagerly loading the export module's
4
+ * embedded template/tool-view text.
5
+ */
6
+
7
+ /** Dark and light TUI theme names bundled into a dual-theme export. */
8
+ export interface ExportThemeNames {
9
+ dark: string;
10
+ light: string;
11
+ }
12
+
13
+ /** Parse `/export [--themes] [path]`; paths containing spaces were never supported. */
14
+ export function parseExportArgs(args: string): { outputPath?: string; useUserThemes: boolean } {
15
+ const parts = args.trim().split(/\s+/).filter(Boolean);
16
+ const useUserThemes = parts.includes("--themes");
17
+ const paths = parts.filter(part => part !== "--themes");
18
+ if (paths.length > 1) throw new Error("Usage: /export [--themes] [path]");
19
+ return { outputPath: paths[0], useUserThemes };
20
+ }
@@ -6,6 +6,7 @@ import { getResolvedThemeColors, getThemeExportColors } from "../../modes/theme/
6
6
  import type { SessionEntry, SessionHeader } from "../../session/session-entries";
7
7
  import { loadEntriesFromFile } from "../../session/session-loader";
8
8
  import { SessionManager } from "../../session/session-manager";
9
+ import type { ExportThemeNames } from "./args";
9
10
  import templateCss from "./template.css" with { type: "text" };
10
11
  import templateHtml from "./template.html" with { type: "text" };
11
12
  import templateJs from "./template.js" with { type: "text" };
@@ -14,6 +15,8 @@ import templateJs from "./template.js" with { type: "text" };
14
15
  import toolViewsJs from "./tool-views.generated.js" with { type: "text" };
15
16
  import { webExportThemeVars } from "./web-palette";
16
17
 
18
+ export { type ExportThemeNames, parseExportArgs } from "./args";
19
+
17
20
  let cachedTemplate: string | undefined;
18
21
 
19
22
  /** Compose the standalone export template: minified CSS, tool renderers, and viewer JS inlined. */
@@ -37,20 +40,12 @@ export function getTemplate(): string {
37
40
 
38
41
  export interface ExportOptions {
39
42
  outputPath?: string;
40
- /**
41
- * Which color palette the export ships with.
42
- * - `"web"` (default) — the omp brand identity (collab-web pink/purple),
43
- * so public HTML exports and the `/s/<id>` share viewer match the live
44
- * `my.omp.sh` client. See `web-palette.ts`.
45
- * - `"theme"` — derive from `themeName` (or the active TUI theme), preserving
46
- * the pre-15.12 behavior where an export mirrored the user's terminal.
47
- */
43
+ /** `"web"` bundles the omp web themes; `"theme"` bundles TUI themes. */
48
44
  palette?: "web" | "theme";
49
- /**
50
- * TUI theme to derive colors from when `palette: "theme"`. Ignored for the
51
- * default `"web"` palette. Resolves to the active TUI theme when omitted.
52
- */
45
+ /** Legacy single TUI theme name. Prefer `themeNames` for dual-theme exports. */
53
46
  themeName?: string;
47
+ /** Dark and light TUI themes to bundle when `palette` is `"theme"`. */
48
+ themeNames?: ExportThemeNames;
54
49
  /** Embed subagent session transcripts found next to the session file (default true). */
55
50
  includeSubSessions?: boolean;
56
51
  }
@@ -116,50 +111,49 @@ function deriveExportColors(baseColor: string): { pageBg: string; cardBg: string
116
111
  }
117
112
 
118
113
  /**
119
- * Generate CSS custom properties for the export `:root`.
120
- *
121
- * Two call shapes:
122
- * • `generateThemeVars("web" | "theme", themeName?)` — explicit palette.
123
- * `"web"` (the default for public artifacts) returns the fixed omp brand
124
- * palette from `web-palette.ts` — collab-web pink/purple identity, shared
125
- * with the live `my.omp.sh` client, so exports and the share viewer render
126
- * identically to it. `"theme"` derives from the TUI theme via
127
- * `getResolvedThemeColors(themeName)` plus the three
128
- * `export.{pageBg,cardBg,infoBg}` surface overrides.
129
- * • `generateThemeVars(themeName)` — legacy single-arg form: derive from the
130
- * named TUI theme. Kept so existing callers (and the theme-islight test)
131
- * keep working; equivalent to `generateThemeVars("theme", themeName)`.
114
+ * Generate CSS custom properties for one export theme.
132
115
  *
133
- * Exported for the share-viewer build script.
116
+ * The single-argument theme-name form remains available to callers that need
117
+ * one TUI palette. Standalone HTML uses `generateThemeStyles()` below.
134
118
  */
135
119
  export async function generateThemeVars(
136
120
  palette: "web" | "theme" | (string & {}) = "web",
137
121
  themeName?: string,
138
122
  ): Promise<string> {
139
- // Legacy single-arg form: `generateThemeVars("my-theme")` the first arg
140
- // is a theme name, not a palette. Route it to the themed path.
141
- if (palette !== "web" && palette !== "theme") {
142
- return generateThemeVars("theme", palette);
143
- }
144
- if (palette === "web") return webExportThemeVars();
123
+ if (palette !== "web" && palette !== "theme") return generateThemeVars("theme", palette);
124
+ if (palette === "web") return webExportThemeVars("dark");
145
125
 
146
126
  const colors = await getResolvedThemeColors(themeName);
147
- const lines: string[] = [];
148
- for (const key in colors) {
149
- lines.push(`--${key}: ${colors[key]};`);
150
- }
151
-
127
+ const lines = Object.entries(colors).map(([key, value]) => `--${key}: ${value};`);
152
128
  const themeExport = await getThemeExportColors(themeName);
153
- const userMessageBg = colors.userMessageBg || "#343541";
154
- const derived = deriveExportColors(userMessageBg);
129
+ const derived = deriveExportColors(colors.userMessageBg || "#343541");
155
130
 
156
131
  lines.push(`--body-bg: ${themeExport.pageBg ?? derived.pageBg};`);
157
132
  lines.push(`--container-bg: ${themeExport.cardBg ?? derived.cardBg};`);
158
133
  lines.push(`--info-bg: ${themeExport.infoBg ?? derived.infoBg};`);
159
-
160
134
  return lines.join(" ");
161
135
  }
162
136
 
137
+ /** Generate dark, light, and auto-following CSS rules for a standalone viewer. */
138
+ export async function generateThemeStyles(
139
+ palette: "web" | "theme",
140
+ themeNames?: ExportThemeNames,
141
+ legacyThemeName?: string,
142
+ ): Promise<string> {
143
+ const [dark, light] =
144
+ palette === "web"
145
+ ? [webExportThemeVars("dark"), webExportThemeVars("light")]
146
+ : await Promise.all([
147
+ generateThemeVars("theme", themeNames?.dark ?? legacyThemeName ?? "dark"),
148
+ generateThemeVars("theme", themeNames?.light ?? legacyThemeName ?? "light"),
149
+ ]);
150
+ return [
151
+ `:root, :root[data-theme="dark"] { color-scheme: dark; ${dark} }`,
152
+ `:root[data-theme="light"] { color-scheme: light; ${light} }`,
153
+ `@media (prefers-color-scheme: light) { :root:not([data-theme]) { color-scheme: light; ${light} } }`,
154
+ ].join("\n");
155
+ }
156
+
163
157
  /** Embedded subagent session transcript, keyed by slash-joined agent path in `SessionData.subSessions`. */
164
158
  export interface SubSession {
165
159
  /** Bare agent id (session file stem), e.g. "ToolAsk". */
@@ -240,15 +234,19 @@ async function collectSubSessionsFromDir(
240
234
  }
241
235
 
242
236
  /** Generate HTML from bundled template with runtime substitutions. */
243
- async function generateHtml(sessionData: SessionData, palette: "web" | "theme", themeName?: string): Promise<string> {
244
- const themeVars = await generateThemeVars(palette, themeName);
237
+ async function generateHtml(
238
+ sessionData: SessionData,
239
+ palette: "web" | "theme",
240
+ themeNames?: ExportThemeNames,
241
+ themeName?: string,
242
+ ): Promise<string> {
243
+ const themeStyles = await generateThemeStyles(palette, themeNames, themeName);
245
244
  const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toBase64();
246
245
 
247
246
  // Use function replacements so `$'`, `$&`, `$$`, `$n`, etc. in the
248
- // substituted CSS/base64 are not interpreted as substitution patterns
249
- // (see https://mdn.io/String.replace).
247
+ // substituted CSS/base64 are not interpreted as substitution patterns.
250
248
  return getTemplate()
251
- .replace("<theme-vars/>", () => `<style>:root { ${themeVars} }</style>`)
249
+ .replace("<theme-vars/>", () => `<style>${themeStyles}</style>`)
252
250
  .replace("{{SESSION_DATA}}", () => sessionDataBase64);
253
251
  }
254
252
 
@@ -270,7 +268,7 @@ export async function exportSessionToHtml(
270
268
  }
271
269
 
272
270
  const palette = opts.palette ?? (opts.themeName ? "theme" : "web");
273
- const html = await generateHtml(sessionData, palette, opts.themeName);
271
+ const html = await generateHtml(sessionData, palette, opts.themeNames, opts.themeName);
274
272
  const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(sessionFile, ".jsonl")}.html`;
275
273
 
276
274
  await Bun.write(outputPath, html);
@@ -300,7 +298,7 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions
300
298
  }
301
299
 
302
300
  const palette = opts.palette ?? (opts.themeName ? "theme" : "web");
303
- const html = await generateHtml(sessionData, palette, opts.themeName);
301
+ const html = await generateHtml(sessionData, palette, opts.themeNames, opts.themeName);
304
302
  const outputPath = opts.outputPath || `${APP_NAME}-session-${path.basename(inputPath, ".jsonl")}.html`;
305
303
 
306
304
  await Bun.write(outputPath, html);
@@ -104,11 +104,14 @@
104
104
  }
105
105
 
106
106
  .sidebar-controls {
107
+ display: flex;
108
+ gap: 6px;
107
109
  padding: 0 0 6px;
108
110
  }
109
111
 
110
112
  .sidebar-search {
111
- width: 100%;
113
+ flex: 1;
114
+ min-width: 0;
112
115
  box-sizing: border-box;
113
116
  padding: 5px 9px;
114
117
  font-size: 11px;
@@ -138,6 +141,21 @@
138
141
  color: var(--muted);
139
142
  }
140
143
 
144
+ .theme-select {
145
+ padding: 5px 8px;
146
+ font: inherit;
147
+ font-size: 10px;
148
+ background: var(--body-bg);
149
+ color: var(--text);
150
+ border: 1px solid var(--hairline);
151
+ border-radius: var(--radius-ctl);
152
+ cursor: pointer;
153
+ }
154
+
155
+ .theme-select:hover {
156
+ border-color: var(--hairline-strong);
157
+ }
158
+
141
159
  .sidebar-filters {
142
160
  display: flex;
143
161
  gap: 4px;
@@ -8,6 +8,7 @@
8
8
  <title>Session Export</title>
9
9
  <template-css/>
10
10
  <theme-vars/>
11
+ <script>try{const t=localStorage.getItem("omp-export-theme");if(t==="light"||t==="dark")document.documentElement.dataset.theme=t}catch{}</script>
11
12
  </head>
12
13
  <body>
13
14
  <button id="hamburger" title="Open sidebar" aria-label="Open sidebar"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true"><path d="M3 6h18M3 12h18M3 18h18"/></svg></button>
@@ -17,6 +18,11 @@
17
18
  <div class="sidebar-header">
18
19
  <div class="sidebar-controls">
19
20
  <input type="text" class="sidebar-search" id="tree-search" placeholder="Search entries…" autocomplete="off" spellcheck="false">
21
+ <select class="theme-select" id="theme-select" title="Color theme" aria-label="Color theme">
22
+ <option value="auto">Auto</option>
23
+ <option value="light">Light</option>
24
+ <option value="dark">Dark</option>
25
+ </select>
20
26
  </div>
21
27
  <div class="sidebar-filters">
22
28
  <button class="filter-btn active" data-filter="default" title="Hide settings entries">Default</button>
@@ -1,6 +1,27 @@
1
1
  (function() {
2
2
  'use strict';
3
3
 
4
+ const THEME_STORAGE_KEY = 'omp-export-theme';
5
+ const themeSelect = document.getElementById('theme-select');
6
+ let themePreference = 'auto';
7
+ try {
8
+ const stored = localStorage.getItem(THEME_STORAGE_KEY);
9
+ if (stored === 'light' || stored === 'dark' || stored === 'auto') themePreference = stored;
10
+ } catch {}
11
+
12
+ function applyThemePreference(next) {
13
+ themePreference = next;
14
+ if (next === 'light' || next === 'dark') document.documentElement.dataset.theme = next;
15
+ else delete document.documentElement.dataset.theme;
16
+ if (themeSelect) themeSelect.value = next;
17
+ try {
18
+ localStorage.setItem(THEME_STORAGE_KEY, next);
19
+ } catch {}
20
+ }
21
+
22
+ applyThemePreference(themePreference);
23
+ if (themeSelect) themeSelect.addEventListener('change', () => applyThemePreference(themeSelect.value));
24
+
4
25
  // ============================================================
5
26
  // BOOT
6
27
  // ============================================================