@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.
- package/CHANGELOG.md +54 -0
- package/README.md +1 -1
- package/dist/cli.js +4659 -4261
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +316 -47
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
- package/dist/types/live/controller.d.ts +40 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +48 -0
- package/dist/types/live/visualizer.d.ts +33 -0
- package/dist/types/lsp/client.d.ts +16 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/settings-defs.d.ts +7 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/interactive-mode.d.ts +5 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +314 -0
- package/dist/types/session/agent-session.d.ts +118 -755
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +49 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +22 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +143 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +5 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +14 -0
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/types.d.ts +94 -0
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +2 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/usage-cli.ts +10 -1
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/model-registry.ts +10 -2
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +209 -51
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +89 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/audio-worklet.txt +59 -0
- package/src/live/browser-runtime.txt +221 -0
- package/src/live/controller.ts +464 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +490 -0
- package/src/live/visualizer.ts +234 -0
- package/src/lsp/client.ts +29 -9
- package/src/lsp/index.ts +19 -4
- package/src/main.ts +8 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/settings-defs.ts +23 -1
- package/src/modes/components/settings-selector.ts +169 -1
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.ts +11 -2
- package/src/modes/controllers/command-controller.ts +56 -15
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/live-command-controller.ts +178 -0
- package/src/modes/controllers/mcp-command-controller.ts +10 -1
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +18 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +157 -38
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +3 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +210 -136
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +334 -0
- package/src/session/agent-session.ts +1892 -12226
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +212 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +263 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.ts +6 -3
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +0 -46
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +943 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +92 -11
- package/src/slash-commands/helpers/usage-report.ts +22 -3
- package/src/system-prompt.ts +41 -17
- package/src/task/executor.ts +285 -70
- package/src/task/index.ts +98 -105
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +1 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +26 -0
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +9 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +7 -1
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +67 -3
- package/src/tts/streaming-player.ts +62 -4
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/xai.ts +7 -1
- 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
|
-
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
|
|
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,
|
|
290
|
-
:
|
|
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
|
+
}
|
package/src/export/html/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
-
|
|
140
|
-
|
|
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
|
|
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
|
|
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(
|
|
244
|
-
|
|
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
|
|
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
|
-
|
|
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
|
// ============================================================
|