mixdog 0.8.0 → 0.9.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/README.md +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +344 -0
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1223 -95
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -98
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -1,38 +1,25 @@
|
|
|
1
1
|
import { isAbsolute, resolve } from 'node:path';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
executionModeSchemaDescription,
|
|
8
|
-
getBackgroundTask,
|
|
9
|
-
renderBackgroundTask,
|
|
10
|
-
renderBackgroundTaskList,
|
|
11
|
-
resolveExecutionMode,
|
|
12
|
-
startBackgroundTask,
|
|
13
|
-
taskIdFromArgs,
|
|
14
|
-
} from '../runtime/shared/background-tasks.mjs';
|
|
15
|
-
import { makeBridgeLlm } from '../runtime/agent/orchestrator/smart-bridge/bridge-llm.mjs';
|
|
3
|
+
import { makeAgentDispatch } from '../runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs';
|
|
4
|
+
import { loadConfig } from '../runtime/agent/orchestrator/config.mjs';
|
|
5
|
+
import { initProviders } from '../runtime/agent/orchestrator/providers/registry.mjs';
|
|
6
|
+
import { presentErrorText } from '../runtime/shared/err-text.mjs';
|
|
16
7
|
|
|
17
8
|
// Ported from the original mixdog tool-defs.mjs `explore` entry.
|
|
18
9
|
// `aiWrapped` is dropped: in the standalone build there is no aiWrapped
|
|
19
10
|
// dispatch hub — execution is wired directly in the runtime executor below
|
|
20
|
-
// via
|
|
11
|
+
// via makeAgentDispatch. The standalone surface is synchronous: it returns a
|
|
21
12
|
// bounded locator result in this tool call.
|
|
22
13
|
export const EXPLORE_TOOL = {
|
|
23
14
|
name: 'explore',
|
|
24
15
|
title: 'Explore',
|
|
25
16
|
annotations: { title: 'Explore', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
26
|
-
description:
|
|
17
|
+
description: 'Repo anchor locator. Use for broad/uncertain paths. Array only for independent targets.',
|
|
27
18
|
inputSchema: {
|
|
28
19
|
type: 'object',
|
|
29
20
|
properties: {
|
|
30
|
-
|
|
31
|
-
cwd: { type: 'string', description: 'Project/root directory
|
|
32
|
-
mode: { type: 'string', enum: ['async', 'sync'], description: `${executionModeSchemaDescription('sync')} Prefer async for non-trivial exploration; choose sync only for an explicit blocking lookup.` },
|
|
33
|
-
action: { type: 'string', enum: ['run', 'list', 'status', 'read', 'cancel'], description: 'Default run. list/status/read/cancel are manual recovery controls for async explore tasks.' },
|
|
34
|
-
task_id: { type: 'string', description: 'Shared background task id for manual status/read/cancel recovery.' },
|
|
35
|
-
background: { type: 'boolean', description: 'Legacy alias for mode=async.' },
|
|
21
|
+
query: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Narrow locator query; array only for independent targets.' },
|
|
22
|
+
cwd: { type: 'string', description: 'Project/root directory.' },
|
|
36
23
|
},
|
|
37
24
|
required: [],
|
|
38
25
|
additionalProperties: false,
|
|
@@ -46,6 +33,12 @@ export const EXPLORE_OUTPUT_CHAR_CAP = 24_000;
|
|
|
46
33
|
const EXPLORE_TRUNCATION_MARKER = '\n\n[explore: output truncated; narrow cwd or split queries to see more]';
|
|
47
34
|
// Bound fan-out so a hostile/poisoned query array cannot spawn unbounded subs.
|
|
48
35
|
export const MAX_FANOUT_QUERIES = 8;
|
|
36
|
+
const EXPLORE_RESULT_CACHE_MAX_ENTRIES = 64;
|
|
37
|
+
const EXPLORE_RESULT_CACHE_TTL_MS = (() => {
|
|
38
|
+
const raw = Number(process.env.MIXDOG_EXPLORE_RESULT_CACHE_TTL_MS);
|
|
39
|
+
return Number.isFinite(raw) && raw >= 0 ? Math.floor(raw) : 5 * 60_000;
|
|
40
|
+
})();
|
|
41
|
+
const EXPLORE_RESULT_CACHE = new Map(); // key -> { ts, value?, promise? }
|
|
49
42
|
|
|
50
43
|
function escapeXml(str) {
|
|
51
44
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
@@ -54,9 +47,9 @@ function escapeXml(str) {
|
|
|
54
47
|
// Mirrors buildExplorerPrompt from the original ai-wrapped-dispatch: each
|
|
55
48
|
// explorer receives ONLY its own query, with a trailing descriptive-only
|
|
56
49
|
// reminder. The full no-verdict contract lives at system level
|
|
57
|
-
// (rules/
|
|
50
|
+
// (rules/agent/30-explorer.md).
|
|
58
51
|
export function buildExplorerPrompt(query) {
|
|
59
|
-
return `<query>${escapeXml(query)}</query>\nReminder:
|
|
52
|
+
return `<query>${escapeXml(query)}</query>\nReminder: output only anchor lines formatted as path:line — symbol/name — short reason, or EXPLORATION_FAILED. No preamble, bullets, numbering, headings, summary, code quotes, analysis, verdicts, ratings, or recommendations.`;
|
|
60
53
|
}
|
|
61
54
|
|
|
62
55
|
export function normalizeExploreQueries(rawQuery) {
|
|
@@ -91,11 +84,28 @@ function settledExplorerResult(result) {
|
|
|
91
84
|
if (result?.status !== 'fulfilled') {
|
|
92
85
|
return { ok: false, text: `[explorer error] ${result?.reason?.message || String(result?.reason)}` };
|
|
93
86
|
}
|
|
94
|
-
const text = typeof result.value === 'string' ? result.value
|
|
87
|
+
const text = cleanExplorerText(typeof result.value === 'string' ? result.value : responseText(result.value));
|
|
95
88
|
if (!text) return { ok: false, text: '[explorer error] empty response' };
|
|
96
89
|
return { ok: true, text };
|
|
97
90
|
}
|
|
98
91
|
|
|
92
|
+
function isFatalExploreError(error) {
|
|
93
|
+
if (!error) return false;
|
|
94
|
+
const status = Number(error.httpStatus || error.status || error.response?.status || 0) || 0;
|
|
95
|
+
if (status === 401 || status === 403 || status === 429) return true;
|
|
96
|
+
if (error.providerQuota === true || error.quotaExceeded === true) return true;
|
|
97
|
+
const code = String(error.code || '').toUpperCase();
|
|
98
|
+
if (code === 'PROVIDER_QUOTA' || code === 'EACCES' || code === 'EAUTH') return true;
|
|
99
|
+
const name = String(error.name || '');
|
|
100
|
+
if (/ProviderQuotaError|Auth|Authentication|Permission/i.test(name)) return true;
|
|
101
|
+
const text = String(error.message || error.reason || error || '');
|
|
102
|
+
return /\b(?:quota|quota_exceeded|insufficient_quota|rate[_ -]?limit|too many requests|resource exhausted|unauthorized|authentication|forbidden|permission denied|invalid api key|token expired)\b/i.test(text);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function fatalExploreError(settled) {
|
|
106
|
+
return (settled || []).find((r) => r?.status === 'rejected' && isFatalExploreError(r.reason))?.reason || null;
|
|
107
|
+
}
|
|
108
|
+
|
|
99
109
|
function mergeSettled(settled, queries) {
|
|
100
110
|
const single = queries.length === 1;
|
|
101
111
|
if (single) {
|
|
@@ -109,7 +119,7 @@ function mergeSettled(settled, queries) {
|
|
|
109
119
|
const sep = '\n\n';
|
|
110
120
|
let truncated = false;
|
|
111
121
|
for (let i = 0; i < settled.length; i++) {
|
|
112
|
-
const header =
|
|
122
|
+
const header = `Q${i + 1}: ${String(queries[i] ?? '').replace(/\s+/g, ' ').slice(0, 60)}`;
|
|
113
123
|
const { text: body } = settledExplorerResult(settled[i]);
|
|
114
124
|
const piece = `${header}\n${body}`;
|
|
115
125
|
const addLen = (parts.length === 0 ? 0 : sep.length) + piece.length;
|
|
@@ -138,6 +148,90 @@ function clean(value) {
|
|
|
138
148
|
return String(value ?? '').trim();
|
|
139
149
|
}
|
|
140
150
|
|
|
151
|
+
function exploreResultCacheEnabled() {
|
|
152
|
+
return EXPLORE_RESULT_CACHE_TTL_MS > 0
|
|
153
|
+
&& !/^(?:0|false|off|no)$/i.test(String(process.env.MIXDOG_EXPLORE_RESULT_CACHE || '1'));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function exploreResultCacheKey({ cwd, presetName, query }) {
|
|
157
|
+
return JSON.stringify({
|
|
158
|
+
cwd: String(cwd || ''),
|
|
159
|
+
presetName: String(presetName || ''),
|
|
160
|
+
query: String(query || ''),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function findConfigPreset(config, presetName) {
|
|
165
|
+
const wanted = clean(presetName);
|
|
166
|
+
if (!wanted) return null;
|
|
167
|
+
return (Array.isArray(config?.presets) ? config.presets : [])
|
|
168
|
+
.find((preset) => clean(preset?.id) === wanted || clean(preset?.name) === wanted)
|
|
169
|
+
|| null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function ensureExploreProviderReady(config) {
|
|
173
|
+
const preset = findConfigPreset(config, config?.maintenance?.explore);
|
|
174
|
+
const provider = clean(preset?.provider);
|
|
175
|
+
if (!provider) return;
|
|
176
|
+
const providers = { ...(config?.providers || {}) };
|
|
177
|
+
providers[provider] = { ...(providers[provider] || {}), enabled: true };
|
|
178
|
+
await initProviders(providers);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function scheduleExploreCodeGraphPrewarm(cwd) {
|
|
182
|
+
if (/^(?:1|true|on|yes)$/i.test(String(process.env.MIXDOG_DISABLE_EXPLORE_CODE_GRAPH_PREWARM || ''))) return;
|
|
183
|
+
void import('../runtime/agent/orchestrator/tools/code-graph.mjs')
|
|
184
|
+
.then((mod) => mod?.prewarmCodeGraphIfProject?.(cwd))
|
|
185
|
+
.catch(() => { /* best-effort */ });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function getCachedExploreResult(key) {
|
|
189
|
+
if (!exploreResultCacheEnabled()) return null;
|
|
190
|
+
const entry = EXPLORE_RESULT_CACHE.get(key);
|
|
191
|
+
if (!entry) return null;
|
|
192
|
+
if (Date.now() - entry.ts > EXPLORE_RESULT_CACHE_TTL_MS) {
|
|
193
|
+
EXPLORE_RESULT_CACHE.delete(key);
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
if (typeof entry.value === 'string') {
|
|
197
|
+
EXPLORE_RESULT_CACHE.delete(key);
|
|
198
|
+
EXPLORE_RESULT_CACHE.set(key, entry);
|
|
199
|
+
return entry.value;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function runExploreCached(key, compute) {
|
|
205
|
+
const cached = getCachedExploreResult(key);
|
|
206
|
+
if (cached !== null) return cached;
|
|
207
|
+
if (!exploreResultCacheEnabled()) return await compute();
|
|
208
|
+
const pending = EXPLORE_RESULT_CACHE.get(key)?.promise;
|
|
209
|
+
if (pending) return await pending;
|
|
210
|
+
const promise = Promise.resolve()
|
|
211
|
+
.then(() => compute())
|
|
212
|
+
.then((value) => {
|
|
213
|
+
const text = typeof value === 'string' ? value : responseText(value);
|
|
214
|
+
const cleaned = cleanExplorerText(text);
|
|
215
|
+
if (cleaned && cleaned !== 'EXPLORATION_FAILED') {
|
|
216
|
+
EXPLORE_RESULT_CACHE.set(key, { ts: Date.now(), value: text });
|
|
217
|
+
while (EXPLORE_RESULT_CACHE.size > EXPLORE_RESULT_CACHE_MAX_ENTRIES) {
|
|
218
|
+
const oldest = EXPLORE_RESULT_CACHE.keys().next().value;
|
|
219
|
+
if (!oldest) break;
|
|
220
|
+
EXPLORE_RESULT_CACHE.delete(oldest);
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
EXPLORE_RESULT_CACHE.delete(key);
|
|
224
|
+
}
|
|
225
|
+
return value;
|
|
226
|
+
})
|
|
227
|
+
.catch((err) => {
|
|
228
|
+
EXPLORE_RESULT_CACHE.delete(key);
|
|
229
|
+
throw err;
|
|
230
|
+
});
|
|
231
|
+
EXPLORE_RESULT_CACHE.set(key, { ts: Date.now(), promise });
|
|
232
|
+
return await promise;
|
|
233
|
+
}
|
|
234
|
+
|
|
141
235
|
function responseText(response) {
|
|
142
236
|
if (typeof response === 'string') return response;
|
|
143
237
|
const parts = Array.isArray(response?.content) ? response.content : [];
|
|
@@ -148,57 +242,49 @@ function responseText(response) {
|
|
|
148
242
|
return text || JSON.stringify(response, null, 2);
|
|
149
243
|
}
|
|
150
244
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
245
|
+
const ANCHOR_LINE_RE = /(?:^|\s)(?:[A-Za-z]:[\\/][^:\n]+|\.{0,2}[\\/][^:\n]+|src[\\/][^:\n]+|[\w.-]+[\\/][^:\n]+):\d+\b/;
|
|
246
|
+
const FAILED_RE = /\b(?:EXPLORATION_FAILED|exploration failed|no credible (?:anchor|location)|no relevant (?:anchor|location)|not found|could(?: not|n't) find)\b/i;
|
|
247
|
+
|
|
248
|
+
function normalizeExplorerLine(line) {
|
|
249
|
+
return String(line || '')
|
|
250
|
+
.trim()
|
|
251
|
+
.replace(/^[-*•]\s+/, '')
|
|
252
|
+
.replace(/^\d+[.)]\s+/, '')
|
|
253
|
+
.replace(/^>\s*/, '')
|
|
254
|
+
.replace(/`/g, '')
|
|
255
|
+
.replace(/\*\*/g, '')
|
|
256
|
+
.trim();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function cleanExplorerText(text) {
|
|
260
|
+
const raw = String(text || '').trim();
|
|
261
|
+
if (!raw) return '';
|
|
262
|
+
const anchors = [];
|
|
263
|
+
for (const sourceLine of raw.split(/\r?\n/)) {
|
|
264
|
+
const line = normalizeExplorerLine(sourceLine);
|
|
265
|
+
if (!line || /^-{3,}$/.test(line) || /^#+\s+/.test(line)) continue;
|
|
266
|
+
if (/^EXPLORATION_FAILED\b/i.test(line)) return 'EXPLORATION_FAILED';
|
|
267
|
+
if (ANCHOR_LINE_RE.test(line)) anchors.push(line);
|
|
160
268
|
}
|
|
161
|
-
|
|
269
|
+
if (anchors.length) return anchors.join('\n');
|
|
270
|
+
if (FAILED_RE.test(raw)) return 'EXPLORATION_FAILED';
|
|
271
|
+
return raw;
|
|
162
272
|
}
|
|
163
273
|
|
|
164
274
|
/**
|
|
165
275
|
* Standalone explore executor.
|
|
166
276
|
*
|
|
167
277
|
* Dispatches the hidden `explorer` read-only filesystem role (one ephemeral
|
|
168
|
-
* sub-session per query) via
|
|
278
|
+
* sub-session per query) via makeAgentDispatch — the same path recall/search-style
|
|
169
279
|
* hidden roles use. Runs query items concurrently (Promise.allSettled), then
|
|
170
280
|
* aggregates the findings into one bounded text result.
|
|
171
281
|
*
|
|
172
|
-
* Runs synchronously
|
|
173
|
-
* shared background task and delivers completion to the owner session.
|
|
282
|
+
* Runs synchronously and returns the bounded locator result in this tool call.
|
|
174
283
|
*
|
|
175
|
-
* @param {object} args — tool args ({ query, cwd
|
|
284
|
+
* @param {object} args — tool args ({ query, cwd }).
|
|
176
285
|
* @param {object} ctx — { callerCwd, callerSessionId }.
|
|
177
286
|
*/
|
|
178
287
|
export async function runExplore(args = {}, ctx = {}) {
|
|
179
|
-
const action = clean(args.action || 'run').toLowerCase();
|
|
180
|
-
if (['list', 'status', 'read', 'cancel'].includes(action)) return controlExploreTask(action, args, ctx);
|
|
181
|
-
|
|
182
|
-
const mode = resolveExecutionMode(args, 'sync');
|
|
183
|
-
if (mode === 'async') {
|
|
184
|
-
const queries = normalizeExploreQueries(args.query);
|
|
185
|
-
if (queries.length === 0) return fail('query is required (one or more non-empty strings)');
|
|
186
|
-
const task = startBackgroundTask({
|
|
187
|
-
surface: 'explore',
|
|
188
|
-
operation: 'explore',
|
|
189
|
-
label: queries[0].replace(/\s+/g, ' ').slice(0, 120),
|
|
190
|
-
input: { query: args.query, cwd: args.cwd || null },
|
|
191
|
-
context: ctx,
|
|
192
|
-
resultType: 'explore_task_result',
|
|
193
|
-
renderResult: responseText,
|
|
194
|
-
run: async () => {
|
|
195
|
-
const response = await runExploreSync(args, ctx);
|
|
196
|
-
if (response?.isError) throw new Error(responseText(response));
|
|
197
|
-
return response;
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
return ok(renderBackgroundTask(task));
|
|
201
|
-
}
|
|
202
288
|
return runExploreSync(args, ctx);
|
|
203
289
|
}
|
|
204
290
|
|
|
@@ -214,18 +300,27 @@ async function runExploreSync(args = {}, ctx = {}) {
|
|
|
214
300
|
}
|
|
215
301
|
|
|
216
302
|
const resolvedCwd = resolveExploreCwd(args.cwd, ctx.callerCwd);
|
|
303
|
+
scheduleExploreCodeGraphPrewarm(resolvedCwd);
|
|
304
|
+
const config = loadConfig();
|
|
305
|
+
await ensureExploreProviderReady(config);
|
|
306
|
+
const presetName = config?.maintenance?.explore || '';
|
|
307
|
+
const llm = makeAgentDispatch({
|
|
308
|
+
role: 'explorer',
|
|
309
|
+
cwd: resolvedCwd,
|
|
310
|
+
brief: true,
|
|
311
|
+
parentSessionId: ctx.callerSessionId || null,
|
|
312
|
+
clientHostPid: ctx.clientHostPid || null,
|
|
313
|
+
config,
|
|
314
|
+
});
|
|
217
315
|
|
|
218
316
|
const settled = await Promise.allSettled(working.map((q) => {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
cwd: resolvedCwd,
|
|
222
|
-
brief: true,
|
|
223
|
-
parentSessionId: ctx.callerSessionId || null,
|
|
224
|
-
clientHostPid: ctx.clientHostPid || null,
|
|
225
|
-
});
|
|
226
|
-
return llm({ prompt: buildExplorerPrompt(q) });
|
|
317
|
+
const key = exploreResultCacheKey({ cwd: resolvedCwd, presetName, query: q });
|
|
318
|
+
return runExploreCached(key, () => llm({ prompt: buildExplorerPrompt(q) }));
|
|
227
319
|
}));
|
|
228
320
|
|
|
321
|
+
const fatal = fatalExploreError(settled);
|
|
322
|
+
if (fatal) return fail(presentErrorText(fatal, { surface: 'explore' }));
|
|
323
|
+
|
|
229
324
|
const merged = mergeSettled(settled, working);
|
|
230
325
|
const allFailed = settled.every((r) => !settledExplorerResult(r).ok);
|
|
231
326
|
const out = capNotice + merged;
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* standalone/folder-dialog.mjs — cross-platform native "choose folder" dialog.
|
|
3
|
+
*
|
|
4
|
+
* Spawns the OS-native directory picker and resolves with the selected absolute
|
|
5
|
+
* path, or null when the user cancels / no dialog tool is available. The TUI
|
|
6
|
+
* falls back to manual path typing when this returns { available:false }.
|
|
7
|
+
*
|
|
8
|
+
* Windows : PowerShell IFileOpenDialog (Explorer-style folder picker)
|
|
9
|
+
* macOS : osascript `choose folder`
|
|
10
|
+
* Linux : zenity --file-selection --directory (fallback: kdialog)
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
const DIALOG_TIMEOUT_MS = 5 * 60 * 1000;
|
|
16
|
+
|
|
17
|
+
/** Run a command, capture stdout, resolve { code, stdout }. Never rejects. */
|
|
18
|
+
function runCapture(cmd, args, { timeoutMs = DIALOG_TIMEOUT_MS } = {}) {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
let child;
|
|
21
|
+
try {
|
|
22
|
+
child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
resolve({ ok: false, code: -1, stdout: '', error });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
let stdout = '';
|
|
28
|
+
let settled = false;
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
if (settled) return;
|
|
31
|
+
settled = true;
|
|
32
|
+
try { child.kill(); } catch { /* ignore */ }
|
|
33
|
+
resolve({ ok: false, code: -1, stdout: '', error: new Error('dialog timed out') });
|
|
34
|
+
}, timeoutMs);
|
|
35
|
+
timer.unref?.();
|
|
36
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
|
|
37
|
+
child.on('error', (error) => {
|
|
38
|
+
if (settled) return;
|
|
39
|
+
settled = true;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
resolve({ ok: false, code: -1, stdout: '', error });
|
|
42
|
+
});
|
|
43
|
+
child.on('close', (code) => {
|
|
44
|
+
if (settled) return;
|
|
45
|
+
settled = true;
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
resolve({ ok: code === 0, code, stdout });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Sentinel: runCapture sets code === -1 ONLY for spawn failures and timeouts
|
|
53
|
+
// (the dialog tool could not actually run). A clean non-zero exit (e.g. the
|
|
54
|
+
// user cancelling) keeps the real exit code, so callers can distinguish
|
|
55
|
+
// "tool unavailable / broken" from "user cancelled".
|
|
56
|
+
function spawnFailed(result) {
|
|
57
|
+
return !!result && result.code === -1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Whether a command exists on PATH (best-effort, non-blocking spawn probe). */
|
|
61
|
+
function commandExists(cmd) {
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const probe = process.platform === 'win32'
|
|
64
|
+
? spawn('where', [cmd], { stdio: 'ignore', windowsHide: true })
|
|
65
|
+
: spawn('which', [cmd], { stdio: 'ignore' });
|
|
66
|
+
probe.on('error', () => resolve(false));
|
|
67
|
+
probe.on('close', (code) => resolve(code === 0));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function psSingleQuoted(value) {
|
|
72
|
+
return String(value ?? '').replace(/'/g, "''");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Absolute directory path for the Windows picker initial folder (best-effort). */
|
|
76
|
+
function resolveInitialPath(initialPath) {
|
|
77
|
+
const trimmed = String(initialPath ?? '').trim();
|
|
78
|
+
try {
|
|
79
|
+
return path.win32.resolve(trimmed || process.cwd());
|
|
80
|
+
} catch {
|
|
81
|
+
return trimmed || process.cwd();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function powershellScript(title, initialPath) {
|
|
86
|
+
// Run STA so the COM file dialog works; print the selected path or nothing.
|
|
87
|
+
const safeTitle = psSingleQuoted(title || 'Select a project folder');
|
|
88
|
+
const safeInitialPath = psSingleQuoted(initialPath || '');
|
|
89
|
+
return [
|
|
90
|
+
// Force UTF-8 stdout so non-ASCII selected paths are not mangled by the
|
|
91
|
+
// console's default code page (PowerShell 5.1 is not UTF-8 by default).
|
|
92
|
+
'$ErrorActionPreference = "Stop"',
|
|
93
|
+
'[Console]::OutputEncoding = [Text.Encoding]::UTF8',
|
|
94
|
+
'Add-Type -AssemblyName System.Windows.Forms | Out-Null',
|
|
95
|
+
`$code = @'
|
|
96
|
+
using System;
|
|
97
|
+
using System.Runtime.InteropServices;
|
|
98
|
+
|
|
99
|
+
namespace Mixdog {
|
|
100
|
+
public static class ExplorerFolderPicker {
|
|
101
|
+
private const int ERROR_CANCELLED = unchecked((int)0x800704C7);
|
|
102
|
+
|
|
103
|
+
[Flags]
|
|
104
|
+
private enum FOS : uint {
|
|
105
|
+
NOCHANGEDIR = 0x00000008,
|
|
106
|
+
PICKFOLDERS = 0x00000020,
|
|
107
|
+
FORCEFILESYSTEM = 0x00000040,
|
|
108
|
+
PATHMUSTEXIST = 0x00000800,
|
|
109
|
+
FILEMUSTEXIST = 0x00001000
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private enum SIGDN : uint {
|
|
113
|
+
FILESYSPATH = 0x80058000
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
[ComImport, Guid("DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7")]
|
|
117
|
+
private class FileOpenDialog {}
|
|
118
|
+
|
|
119
|
+
[ComImport, Guid("42f85136-db7e-439c-85f1-e4075d135fc8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
120
|
+
private interface IFileDialog {
|
|
121
|
+
[PreserveSig] int Show(IntPtr parent);
|
|
122
|
+
[PreserveSig] int SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec);
|
|
123
|
+
[PreserveSig] int SetFileTypeIndex(uint iFileType);
|
|
124
|
+
[PreserveSig] int GetFileTypeIndex(out uint piFileType);
|
|
125
|
+
[PreserveSig] int Advise(IntPtr pfde, out uint pdwCookie);
|
|
126
|
+
[PreserveSig] int Unadvise(uint dwCookie);
|
|
127
|
+
[PreserveSig] int SetOptions(FOS fos);
|
|
128
|
+
[PreserveSig] int GetOptions(out FOS pfos);
|
|
129
|
+
[PreserveSig] int SetDefaultFolder(IShellItem psi);
|
|
130
|
+
[PreserveSig] int SetFolder(IShellItem psi);
|
|
131
|
+
[PreserveSig] int GetFolder(out IShellItem ppsi);
|
|
132
|
+
[PreserveSig] int GetCurrentSelection(out IShellItem ppsi);
|
|
133
|
+
[PreserveSig] int SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName);
|
|
134
|
+
[PreserveSig] int GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
|
|
135
|
+
[PreserveSig] int SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
|
|
136
|
+
[PreserveSig] int SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText);
|
|
137
|
+
[PreserveSig] int SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
|
|
138
|
+
[PreserveSig] int GetResult(out IShellItem ppsi);
|
|
139
|
+
[PreserveSig] int AddPlace(IShellItem psi, int fdap);
|
|
140
|
+
[PreserveSig] int SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
|
|
141
|
+
[PreserveSig] int Close(int hr);
|
|
142
|
+
[PreserveSig] int SetClientGuid(ref Guid guid);
|
|
143
|
+
[PreserveSig] int ClearClientData();
|
|
144
|
+
[PreserveSig] int SetFilter(IntPtr pFilter);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
[ComImport, Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
148
|
+
private interface IShellItem {
|
|
149
|
+
[PreserveSig] int BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
|
|
150
|
+
[PreserveSig] int GetParent(out IShellItem ppsi);
|
|
151
|
+
[PreserveSig] int GetDisplayName(SIGDN sigdnName, out IntPtr ppszName);
|
|
152
|
+
[PreserveSig] int GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
|
|
153
|
+
[PreserveSig] int Compare(IShellItem psi, uint hint, out int piOrder);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
[DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
|
|
157
|
+
private static extern void SHCreateItemFromParsingName(
|
|
158
|
+
[MarshalAs(UnmanagedType.LPWStr)] string pszPath,
|
|
159
|
+
IntPtr pbc,
|
|
160
|
+
ref Guid riid,
|
|
161
|
+
out IShellItem ppv
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
165
|
+
private struct RECT {
|
|
166
|
+
public int Left;
|
|
167
|
+
public int Top;
|
|
168
|
+
public int Right;
|
|
169
|
+
public int Bottom;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
[DllImport("user32.dll")]
|
|
173
|
+
private static extern IntPtr GetForegroundWindow();
|
|
174
|
+
|
|
175
|
+
[DllImport("kernel32.dll")]
|
|
176
|
+
private static extern IntPtr GetConsoleWindow();
|
|
177
|
+
|
|
178
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
179
|
+
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
|
|
180
|
+
|
|
181
|
+
public static int[] GetDialogOwnerCenter() {
|
|
182
|
+
IntPtr hwnd = GetConsoleWindow();
|
|
183
|
+
if (hwnd == IntPtr.Zero) hwnd = GetForegroundWindow();
|
|
184
|
+
RECT rect;
|
|
185
|
+
if (hwnd != IntPtr.Zero && GetWindowRect(hwnd, out rect)) {
|
|
186
|
+
return new int[] { (rect.Left + rect.Right) / 2, (rect.Top + rect.Bottom) / 2 };
|
|
187
|
+
}
|
|
188
|
+
var area = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea;
|
|
189
|
+
return new int[] { area.Left + (area.Width / 2), area.Top + (area.Height / 2) };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public static string Pick(string title, string initialPath, IntPtr owner) {
|
|
193
|
+
IFileDialog dialog = null;
|
|
194
|
+
IShellItem item = null;
|
|
195
|
+
IShellItem initialItem = null;
|
|
196
|
+
IntPtr pathPtr = IntPtr.Zero;
|
|
197
|
+
try {
|
|
198
|
+
dialog = (IFileDialog)new FileOpenDialog();
|
|
199
|
+
FOS options;
|
|
200
|
+
ThrowIfFailed(dialog.GetOptions(out options));
|
|
201
|
+
ThrowIfFailed(dialog.SetOptions(options | FOS.PICKFOLDERS | FOS.FORCEFILESYSTEM | FOS.PATHMUSTEXIST | FOS.FILEMUSTEXIST | FOS.NOCHANGEDIR));
|
|
202
|
+
if (!String.IsNullOrEmpty(title)) ThrowIfFailed(dialog.SetTitle(title));
|
|
203
|
+
if (!String.IsNullOrEmpty(initialPath) && System.IO.Directory.Exists(initialPath)) {
|
|
204
|
+
try {
|
|
205
|
+
Guid shellItemId = new Guid("43826d1e-e718-42ee-bc55-a1e261c37bfe");
|
|
206
|
+
SHCreateItemFromParsingName(initialPath, IntPtr.Zero, ref shellItemId, out initialItem);
|
|
207
|
+
if (initialItem != null) {
|
|
208
|
+
ThrowIfFailed(dialog.SetDefaultFolder(initialItem));
|
|
209
|
+
ThrowIfFailed(dialog.SetFolder(initialItem));
|
|
210
|
+
}
|
|
211
|
+
} catch {
|
|
212
|
+
// Initial folder is a convenience only; still show the picker.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
int hr = dialog.Show(owner);
|
|
216
|
+
if (hr == ERROR_CANCELLED) return null;
|
|
217
|
+
ThrowIfFailed(hr);
|
|
218
|
+
ThrowIfFailed(dialog.GetResult(out item));
|
|
219
|
+
ThrowIfFailed(item.GetDisplayName(SIGDN.FILESYSPATH, out pathPtr));
|
|
220
|
+
return Marshal.PtrToStringUni(pathPtr);
|
|
221
|
+
} finally {
|
|
222
|
+
if (pathPtr != IntPtr.Zero) Marshal.FreeCoTaskMem(pathPtr);
|
|
223
|
+
if (initialItem != null) Marshal.ReleaseComObject(initialItem);
|
|
224
|
+
if (item != null) Marshal.ReleaseComObject(item);
|
|
225
|
+
if (dialog != null) Marshal.ReleaseComObject(dialog);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private static void ThrowIfFailed(int hr) {
|
|
230
|
+
if (hr < 0) Marshal.ThrowExceptionForHR(hr);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
'@`,
|
|
235
|
+
'Add-Type -TypeDefinition $code -ReferencedAssemblies System.Windows.Forms | Out-Null',
|
|
236
|
+
// Invisible TopMost owner anchored to the TUI console (or foreground window)
|
|
237
|
+
// so IFileOpenDialog is modal, centered, and not detached on another monitor.
|
|
238
|
+
'$owner = New-Object System.Windows.Forms.Form',
|
|
239
|
+
'$owner.TopMost = $true',
|
|
240
|
+
'$owner.ShowInTaskbar = $false',
|
|
241
|
+
'$owner.FormBorderStyle = "None"',
|
|
242
|
+
'$owner.StartPosition = "Manual"',
|
|
243
|
+
'$owner.Width = 1',
|
|
244
|
+
'$owner.Height = 1',
|
|
245
|
+
'$owner.Opacity = 0',
|
|
246
|
+
'$ownerCenter = [Mixdog.ExplorerFolderPicker]::GetDialogOwnerCenter()',
|
|
247
|
+
'$owner.Left = [int]$ownerCenter[0]',
|
|
248
|
+
'$owner.Top = [int]$ownerCenter[1]',
|
|
249
|
+
'$owner.Show()',
|
|
250
|
+
'$owner.Activate()',
|
|
251
|
+
'try {',
|
|
252
|
+
` $path = [Mixdog.ExplorerFolderPicker]::Pick('${safeTitle}', '${safeInitialPath}', $owner.Handle)`,
|
|
253
|
+
' if ($path) { [Console]::Out.Write($path) }',
|
|
254
|
+
'} finally {',
|
|
255
|
+
' $owner.Dispose()',
|
|
256
|
+
'}',
|
|
257
|
+
].join('\n');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Open the native folder picker.
|
|
262
|
+
* @returns {Promise<{ available: boolean, path: string|null }>}
|
|
263
|
+
* available=false → no usable dialog tool; caller should fall back to typing.
|
|
264
|
+
* path=null with available=true → the user cancelled.
|
|
265
|
+
*/
|
|
266
|
+
export async function pickFolder({ title = 'Select a project folder', initialPath = '' } = {}) {
|
|
267
|
+
const platform = process.platform;
|
|
268
|
+
|
|
269
|
+
if (platform === 'win32') {
|
|
270
|
+
const resolvedInitial = resolveInitialPath(initialPath);
|
|
271
|
+
const result = await runCapture('powershell.exe', [
|
|
272
|
+
'-NoLogo', '-NoProfile', '-NonInteractive', '-STA',
|
|
273
|
+
'-Command', powershellScript(title, resolvedInitial),
|
|
274
|
+
]);
|
|
275
|
+
// Could not run PowerShell / Common File Dialog → fall back to manual typing.
|
|
276
|
+
if (spawnFailed(result)) return { available: false, path: null };
|
|
277
|
+
const path = String(result.stdout || '').trim();
|
|
278
|
+
if (!result.ok && !path) return { available: false, path: null };
|
|
279
|
+
return { available: true, path: path || null };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (platform === 'darwin') {
|
|
283
|
+
const safeTitle = String(title).replace(/"/g, '\\"');
|
|
284
|
+
const script = `set f to choose folder with prompt "${safeTitle}"\nPOSIX path of f`;
|
|
285
|
+
const result = await runCapture('osascript', ['-e', script]);
|
|
286
|
+
// Could not run osascript (spawn failure / timeout) → manual fallback.
|
|
287
|
+
if (spawnFailed(result)) return { available: false, path: null };
|
|
288
|
+
// osascript exits non-zero (code 1) on user cancel; treat as cancel.
|
|
289
|
+
const path = String(result.stdout || '').trim();
|
|
290
|
+
return { available: true, path: path || null };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Linux / other unix: prefer zenity, then kdialog.
|
|
294
|
+
if (await commandExists('zenity')) {
|
|
295
|
+
const result = await runCapture('zenity', [
|
|
296
|
+
'--file-selection', '--directory', `--title=${title}`,
|
|
297
|
+
]);
|
|
298
|
+
// Spawn failure / timeout (broken display, missing portal, etc.) → manual
|
|
299
|
+
// fallback rather than silently looping back to the picker.
|
|
300
|
+
if (spawnFailed(result)) return { available: false, path: null };
|
|
301
|
+
const path = String(result.stdout || '').trim();
|
|
302
|
+
if (!result.ok && !path) return { available: true, path: null }; // cancel
|
|
303
|
+
return { available: true, path: path || null };
|
|
304
|
+
}
|
|
305
|
+
if (await commandExists('kdialog')) {
|
|
306
|
+
const result = await runCapture('kdialog', ['--getexistingdirectory', '.']);
|
|
307
|
+
if (spawnFailed(result)) return { available: false, path: null };
|
|
308
|
+
const path = String(result.stdout || '').trim();
|
|
309
|
+
if (!result.ok && !path) return { available: true, path: null };
|
|
310
|
+
return { available: true, path: path || null };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return { available: false, path: null };
|
|
314
|
+
}
|