mixdog 1.1.2 → 1.2.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 +1 -0
- package/package.json +2 -1
- package/scripts/lib/run-node-tests.mjs +9 -0
- package/scripts/prune-desktop-runtime.mjs +3 -0
- package/scripts/test-direct.mjs +4 -2
- package/src/defaults/shipped-history.json +6 -0
- package/src/defaults/skills/setup/references/actions.md +5 -2
- package/src/defaults/skills/visualize/SKILL.md +157 -0
- package/src/defaults/skills/visualize/references/craft.md +88 -0
- package/src/headless-command.mjs +56 -3
- package/src/headless-exec.mjs +17 -1
- package/src/help.mjs +2 -0
- package/src/rules/shared/70-delivery.md +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch/preset.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +1 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-presets.mjs +2 -0
- package/src/runtime/agent/orchestrator/config-providers-merge.mjs +17 -11
- package/src/runtime/agent/orchestrator/config-storage.mjs +1 -0
- package/src/runtime/agent/orchestrator/config.mjs +9 -1
- package/src/runtime/agent/orchestrator/mcp/client-config.mjs +47 -7
- package/src/runtime/agent/orchestrator/mcp/client-handshake.mjs +51 -8
- package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +25 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +348 -122
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +4 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +13 -1
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/cursor-wire-normalization.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/effort-configuration.mjs +12 -4
- package/src/runtime/agent/orchestrator/providers/effort-rejection.mjs +244 -0
- package/src/runtime/agent/orchestrator/providers/grok-client-version.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-profile.mjs +8 -40
- package/src/runtime/agent/orchestrator/providers/mimo-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/mimo-oauth.mjs +51 -12
- package/src/runtime/agent/orchestrator/providers/model-cache.mjs +37 -2
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.d.mts +67 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-channel.mjs +521 -0
- package/src/runtime/agent/orchestrator/providers/model-proxy-provider.mjs +251 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth-tokens.mjs +2 -0
- package/src/runtime/agent/orchestrator/providers/muse-code-oauth.mjs +69 -102
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +5 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/registry.mjs +15 -1
- package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +10 -0
- package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +36 -3
- package/src/runtime/agent/orchestrator/runtime-core/effort.mjs +12 -11
- package/src/runtime/agent/orchestrator/runtime-core/model-capabilities.mjs +16 -1
- package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +11 -0
- package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -3
- package/src/runtime/agent/orchestrator/runtime-core/workflow.mjs +10 -0
- package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +9 -5
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +18 -8
- package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +18 -11
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +13 -5
- package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +362 -0
- package/src/runtime/agent/orchestrator/session/compaction-archive.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/continuation.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/diagnostic.mjs +1 -7
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/refusal.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn/segments.mjs +3 -2
- package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +17 -7
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +12 -2
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-auto-effort.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +5 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +5 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -16
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +90 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -12
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +32 -3
- package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +27 -0
- package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/message-ids.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/message-ids.test-support.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +29 -19
- package/src/runtime/agent/orchestrator/session/store/session-delete.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store/sweep/sweep-finish.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-agent-worker-rows.mjs +11 -0
- package/src/runtime/agent/orchestrator/session/store-summary-fields.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +23 -4
- package/src/runtime/agent/orchestrator/session/store-transcript-projection.mjs +20 -3
- package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +28 -8
- package/src/runtime/agent/orchestrator/session/transcript-restore/user-text.mjs +140 -0
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +52 -2
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +32 -22
- package/src/runtime/agent/orchestrator/tools/builtin/git-repo-rw-lock.mjs +10 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-cache-policy.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +31 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-fast-paths.mjs +32 -35
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-render.mjs +11 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-stream.mjs +9 -6
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -66
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +8 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-source-windows.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +16 -7
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-dispatch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-publication.mjs +137 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool/batch-snapshot.mjs +11 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +21 -11
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +56 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +58 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/trusted-roots.mjs +25 -1
- package/src/runtime/agent/orchestrator/tools/patch/ui-diff.mjs +78 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +2 -0
- package/src/runtime/attachments/store.mjs +38 -0
- package/src/runtime/shared/atomic-file.mjs +8 -2
- package/src/runtime/shared/best-effort-stderr.mjs +8 -0
- package/src/runtime/shared/code-graph-state.mjs +12 -0
- package/src/runtime/shared/defaults-separation.mjs +2 -0
- package/src/runtime/shared/git-runner-worker.mjs +3 -2
- package/src/runtime/shared/git-runner.mjs +4 -3
- package/src/runtime/shared/llm/default-context-window.mjs +6 -0
- package/src/runtime/shared/llm/model-catalog-projection.mjs +5 -4
- package/src/runtime/shared/llm/model-catalog.mjs +240 -118
- package/src/runtime/shared/llm/provider-catalog-cache.mjs +36 -28
- package/src/runtime/shared/llm/provider-catalog-registry.mjs +156 -0
- package/src/runtime/shared/llm/provider-seed-rosters.mjs +130 -0
- package/src/runtime/shared/model-proxy-mode.d.mts +3 -0
- package/src/runtime/shared/model-proxy-mode.mjs +19 -0
- package/src/runtime/shared/provider-accounts.mjs +2 -2
- package/src/runtime/shared/provider-api-key.mjs +21 -1
- package/src/runtime/shared/schedule-model-ref.mjs +11 -2
- package/src/runtime/shared/session-usage-totals.mjs +19 -0
- package/src/runtime/shared/sharp-electron-linux.mjs +25 -0
- package/src/runtime/shared/singleton-owner.mjs +36 -6
- package/src/runtime/shared/statusline/statusline-segments.mjs +68 -14
- package/src/runtime/shared/tool-surface.mjs +7 -0
- package/src/runtime/shared/tool-work-units.mjs +14 -0
- package/src/runtime/shared/transcript-metadata.mjs +17 -1
- package/src/runtime/shared/turn-changes.mjs +791 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +346 -12
- package/src/runtime/shared/turn-snapshot.mjs +690 -81
- package/src/runtime/shared/turn-worktree-snapshot.mjs +161 -11
- package/src/runtime/shared/worktrees/cache-cleanups.mjs +52 -0
- package/src/runtime/shared/worktrees/contract.mjs +551 -0
- package/src/runtime/shared/worktrees/disk.mjs +57 -0
- package/src/runtime/shared/worktrees/git.mjs +247 -0
- package/src/runtime/shared/worktrees/include.mjs +480 -0
- package/src/runtime/shared/worktrees/lock.mjs +46 -0
- package/src/runtime/shared/worktrees/maintenance.mjs +96 -0
- package/src/runtime/shared/worktrees/manager.mjs +1215 -0
- package/src/runtime/shared/worktrees/merge.mjs +182 -0
- package/src/runtime/shared/worktrees/owner.mjs +46 -0
- package/src/runtime/shared/worktrees/paths.mjs +62 -0
- package/src/runtime/shared/worktrees/registry.mjs +110 -0
- package/src/runtime/shared/worktrees/settings.mjs +84 -0
- package/src/runtime/shared/worktrees/slug.mjs +62 -0
- package/src/runtime/shared/worktrees/test-support.mjs +96 -0
- package/src/runtime/visualize-bridge/client.mjs +181 -0
- package/src/runtime/visualize-bridge/tool-defs.mjs +97 -0
- package/src/session-runtime/boot/apis.mjs +35 -0
- package/src/session-runtime/boot/config.mjs +2 -0
- package/src/session-runtime/boot/facade.mjs +20 -0
- package/src/session-runtime/boot/infrastructure.mjs +1 -0
- package/src/session-runtime/boot/session.mjs +1 -0
- package/src/session-runtime/boot/tools.mjs +4 -0
- package/src/session-runtime/config-lifecycle.mjs +10 -1
- package/src/session-runtime/cwd-plugins/cwd-apply.mjs +11 -1
- package/src/session-runtime/global-extensions.mjs +2 -2
- package/src/session-runtime/internal-tool-executor/bridge-tools.mjs +27 -2
- package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +32 -1
- package/src/session-runtime/internal-tool-executor.mjs +9 -1
- package/src/session-runtime/lifecycle/session-catalog.mjs +8 -1
- package/src/session-runtime/lifecycle/session-switch/context-switch.mjs +5 -0
- package/src/session-runtime/lifecycle/session-switch/resume.mjs +21 -3
- package/src/session-runtime/lifecycle/session-switch/session-delete.mjs +16 -0
- package/src/session-runtime/mcp-glue/connect.mjs +11 -6
- package/src/session-runtime/mcp-glue/server-catalog.mjs +7 -3
- package/src/session-runtime/mcp-glue/server-input.mjs +29 -0
- package/src/session-runtime/model-route/route-persist.mjs +3 -0
- package/src/session-runtime/model-route/set-route.mjs +15 -2
- package/src/session-runtime/notification-bus.mjs +9 -1
- package/src/session-runtime/provider-models/row-hydration.mjs +2 -0
- package/src/session-runtime/resource-extension-sync.mjs +30 -7
- package/src/session-runtime/resource-mcp-api.mjs +20 -7
- package/src/session-runtime/route-resolution.mjs +10 -1
- package/src/session-runtime/route-state.mjs +16 -2
- package/src/session-runtime/runtime-core.mjs +5 -0
- package/src/session-runtime/runtime-facade.mjs +8 -0
- package/src/session-runtime/runtime-review-api.mjs +9 -0
- package/src/session-runtime/services/agent-tool/helpers.mjs +5 -0
- package/src/session-runtime/services/agent-tool/job-views/agent-list.mjs +3 -0
- package/src/session-runtime/services/agent-tool/job-views/job-lookup.mjs +1 -0
- package/src/session-runtime/services/agent-tool/job-views/spawn-meta.mjs +1 -0
- package/src/session-runtime/services/agent-tool/lead-worker-index.mjs +15 -0
- package/src/session-runtime/services/agent-tool/send-flow.mjs +1 -0
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +10 -2
- package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +19 -7
- package/src/session-runtime/services/agent-tool/spawn-preset.mjs +3 -0
- package/src/session-runtime/services/agent-tool/tag-registry.mjs +2 -1
- package/src/session-runtime/services/agent-tool/worker-index/row-shape.mjs +5 -0
- package/src/session-runtime/services/agent-tool/worker-rows.mjs +1 -0
- package/src/session-runtime/services/agent-tool.mjs +1 -0
- package/src/session-runtime/services/daemon-candidate.mjs +3 -2
- package/src/session-runtime/services/daemon-crash-capture/spawn-capture.mjs +55 -0
- package/src/session-runtime/session-create/session-options.mjs +10 -1
- package/src/session-runtime/session-turn-api.mjs +1 -0
- package/src/session-runtime/settings-api.mjs +4 -2
- package/src/session-runtime/settings-builtin-tools-api.mjs +9 -2
- package/src/session-runtime/settings-worktree-api.mjs +17 -0
- package/src/session-runtime/setup-tool/executor.mjs +11 -4
- package/src/session-runtime/setup-tool/settings-contract.mjs +3 -1
- package/src/session-runtime/setup-tool/tool-defs.mjs +31 -3
- package/src/session-runtime/turn/agent-ops.mjs +12 -1
- package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +13 -3
- package/src/session-runtime/turn/turn-run/snapshot-tracker.mjs +20 -15
- package/src/session-runtime/turn/turn-run/turn-open.mjs +4 -5
- package/src/session-runtime/turn/turn-run/turn-prepare.mjs +29 -1
- package/src/session-runtime/turn/turn-run/turn-settle.mjs +6 -1
- package/src/session-runtime/turn/turn-run.mjs +6 -7
- package/src/session-runtime/workflow-agents-api/agent-route.mjs +15 -3
- package/src/session-runtime/workspace/admission-gate.mjs +197 -0
- package/src/session-runtime/workspace/project-branches.mjs +136 -0
- package/src/session-runtime/workspace/transition-steps.mjs +364 -0
- package/src/session-runtime/workspace/transitions.mjs +651 -0
- package/src/session-runtime/workspace/workspace-state.mjs +112 -0
- package/src/standalone/channel-transport/http-routes.mjs +17 -0
- package/src/standalone/channel-transport.mjs +13 -1
- package/src/standalone/daemon-desktop-runtime.mjs +1 -0
- package/src/standalone/daemon-owner.mjs +372 -0
- package/src/standalone/daemon-stored-session-views.mjs +4 -1
- package/src/standalone/daemon.mjs +139 -5
- package/src/standalone/desktop-service-registry.mjs +20 -0
- package/src/standalone/local-session-runtime.mjs +11 -0
- package/src/standalone/session-client/attachment-calls.mjs +3 -1
- package/src/standalone/session-client/daemon-unavailable.mjs +62 -0
- package/src/standalone/session-client/lease-heartbeat.mjs +47 -0
- package/src/standalone/session-client.mjs +104 -13
- package/src/standalone/session-protocol.mjs +16 -0
- package/src/standalone/session-proxy.mjs +3 -0
- package/src/standalone/session-service/agent-tree/agent-child.mjs +4 -0
- package/src/standalone/session-service/agent-tree/agent-rehydrate.mjs +1 -0
- package/src/standalone/session-service/entries/lifecycle.mjs +6 -0
- package/src/standalone/session-service/projection/revisions.mjs +2 -0
- package/src/standalone/session-service/projection/transcript-window.mjs +149 -19
- package/src/standalone/session-service/projection/workspace.mjs +14 -0
- package/src/standalone/session-service/session-calls/session-action.mjs +11 -3
- package/src/standalone/session-transport/call-route.mjs +13 -0
- package/src/standalone/session-transport/client-registry/client-grace.mjs +45 -3
- package/src/standalone/session-transport/client-registry/registration-replays.mjs +1 -0
- package/src/standalone/session-transport/client-registry.mjs +20 -3
- package/src/standalone/session-transport/routes.mjs +79 -2
- package/src/standalone/session-transport/stack.mjs +2 -0
- package/src/standalone/session-transport.mjs +24 -1
- package/src/tui/app/message-selector.mjs +2 -1
- package/src/tui/app/model-options.mjs +35 -5
- package/src/tui/app/model-picker/provider-models-list.mjs +2 -2
- package/src/tui/app/model-picker.mjs +4 -2
- package/src/tui/dist/index.mjs +332 -36
- package/src/tui/index.jsx +10 -2
- package/src/tui/session/compaction-view.mjs +147 -0
- package/src/tui/session/context-state.mjs +6 -0
- package/src/tui/session/initial-state.mjs +3 -0
- package/src/tui/session/quota-resume.mjs +2 -1
- package/src/tui/session/session-api/commands.mjs +10 -2
- package/src/tui/session/session-api/controls.mjs +4 -0
- package/src/tui/session/session-api/intake/rewind.mjs +2 -1
- package/src/tui/session/session-api/integrations.mjs +17 -0
- package/src/tui/session/session-api/lifecycle/dispose.mjs +6 -1
- package/src/tui/session/session-api/lifecycle/inheritance.mjs +2 -0
- package/src/tui/session/session-api/lifecycle/resume.mjs +3 -0
- package/src/tui/session/session-api/routes.mjs +10 -1
- package/src/tui/session/session-api/settings.mjs +2 -0
- package/src/tui/session/session-api/worktree.mjs +60 -0
- package/src/tui/session/session-api.mjs +2 -0
- package/src/tui/session/session-flow/auto-clear.mjs +31 -12
- package/src/tui/session/session-flow/drain.mjs +15 -1
- package/src/tui/session/session-flow/session-reset.mjs +4 -3
- package/src/tui/session/session-flow.mjs +2 -0
- package/src/tui/session/transcript-intake.mjs +14 -5
- package/src/tui/session/turn-stream.mjs +32 -4
- package/src/tui/session/turn-tool-cards.mjs +10 -2
- package/src/tui/session/turn.mjs +39 -10
|
@@ -91,14 +91,27 @@ export function createCallRoute({ handleCall, clients, lanes, callCache, nowMs }
|
|
|
91
91
|
// tell a failed CALL from a dead TRANSPORT (which must re-attach). The
|
|
92
92
|
// machine-readable `code` (ECALLIDCONFLICT / ECALLRESULTDROPPED) travels
|
|
93
93
|
// with it: a client must be able to branch on it, not parse prose.
|
|
94
|
+
// Typed (worktree) failures also carry their plain JSON `details`.
|
|
94
95
|
sendJson(
|
|
95
96
|
res,
|
|
96
97
|
{
|
|
97
98
|
error: err?.message || String(err),
|
|
98
99
|
...(err?.code ? { code: String(err.code) } : {}),
|
|
100
|
+
...jsonDetails(err),
|
|
99
101
|
},
|
|
100
102
|
200
|
|
101
103
|
);
|
|
102
104
|
}
|
|
103
105
|
};
|
|
104
106
|
}
|
|
107
|
+
|
|
108
|
+
/** `{ details }` when the error's details are a JSON-safe object, else {}. */
|
|
109
|
+
function jsonDetails(err) {
|
|
110
|
+
const details = err?.details;
|
|
111
|
+
if (!details || typeof details !== 'object') return {};
|
|
112
|
+
try {
|
|
113
|
+
return { details: JSON.parse(JSON.stringify(details)) };
|
|
114
|
+
} catch {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -5,11 +5,44 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { isPidAlive } from '../../../runtime/shared/pid-liveness.mjs';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
const LEASE_MIN_MS = 3_000;
|
|
9
|
+
const LEASE_MAX_MS = 600_000;
|
|
10
|
+
const LEASE_DEFAULT_MS = 30_000;
|
|
11
|
+
const REMOTE_CLIENT_GRACE_DEFAULT_MS = 1_800_000;
|
|
12
|
+
|
|
13
|
+
/** A remote client's lease: the requested `leaseMs`, clamped to 3s..10min. */
|
|
14
|
+
export function clampLeaseMs(value) {
|
|
15
|
+
const leaseMs = Number(value);
|
|
16
|
+
if (!Number.isFinite(leaseMs) || leaseMs <= 0) return LEASE_DEFAULT_MS;
|
|
17
|
+
return Math.min(LEASE_MAX_MS, Math.max(LEASE_MIN_MS, leaseMs));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// A remote desktop behind a flaky tunnel needs far longer to come back than a
|
|
21
|
+
// local terminal restarting. Read at arm time so the env can change per run.
|
|
22
|
+
function remoteClientGraceMs() {
|
|
23
|
+
const configured = Number(process.env.MIXDOG_REMOTE_CLIENT_GRACE_MS);
|
|
24
|
+
return Number.isFinite(configured) && configured >= 0 && process.env.MIXDOG_REMOTE_CLIENT_GRACE_MS
|
|
25
|
+
? configured
|
|
26
|
+
: REMOTE_CLIENT_GRACE_DEFAULT_MS;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createClientGrace({
|
|
30
|
+
clients,
|
|
31
|
+
log,
|
|
32
|
+
clientGraceMs,
|
|
33
|
+
sweepMs,
|
|
34
|
+
nowMs = () => Date.now(),
|
|
35
|
+
onClientsEmpty,
|
|
36
|
+
isClosed,
|
|
37
|
+
dropClient,
|
|
38
|
+
}) {
|
|
9
39
|
let graceTimer = null;
|
|
10
40
|
let sweepTimer = null;
|
|
11
41
|
let everHadLifecycleClient = false;
|
|
12
42
|
let signalled = false;
|
|
43
|
+
// Whether the most recently departed lifecycle client was remote; it picks
|
|
44
|
+
// the empty-clients grace.
|
|
45
|
+
let lastLifecycleWasRemote = false;
|
|
13
46
|
|
|
14
47
|
function lifecycleClientCount() {
|
|
15
48
|
let count = 0;
|
|
@@ -45,10 +78,14 @@ export function createClientGrace({ clients, log, clientGraceMs, sweepMs, onClie
|
|
|
45
78
|
try {
|
|
46
79
|
onClientsEmpty();
|
|
47
80
|
} catch {}
|
|
48
|
-
}, clientGraceMs);
|
|
81
|
+
}, lastLifecycleWasRemote ? remoteClientGraceMs() : clientGraceMs);
|
|
49
82
|
graceTimer.unref?.();
|
|
50
83
|
}
|
|
51
84
|
|
|
85
|
+
function noteLifecycleDropped(client) {
|
|
86
|
+
lastLifecycleWasRemote = client.remote === true;
|
|
87
|
+
}
|
|
88
|
+
|
|
52
89
|
/** A lifecycle client holds the daemon up: remember one ever attached and
|
|
53
90
|
* cancel any pending grace. */
|
|
54
91
|
function noteLifecycleClient() {
|
|
@@ -61,6 +98,11 @@ export function createClientGrace({ clients, log, clientGraceMs, sweepMs, onClie
|
|
|
61
98
|
if (sweepTimer || isClosed()) return;
|
|
62
99
|
sweepTimer = setInterval(() => {
|
|
63
100
|
for (const [token, c] of [...clients]) {
|
|
101
|
+
if (c.remote) {
|
|
102
|
+
// Its owning process is on another machine: liveness is the lease.
|
|
103
|
+
if (nowMs() - c.lastSeen > c.leaseMs) dropClient(token, 'lease expired');
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
64
106
|
// A client whose owning process died can never read its stream again.
|
|
65
107
|
if (c.leadPid && !isPidAlive(c.leadPid)) dropClient(token, 'lead pid gone');
|
|
66
108
|
}
|
|
@@ -77,5 +119,5 @@ export function createClientGrace({ clients, log, clientGraceMs, sweepMs, onClie
|
|
|
77
119
|
}
|
|
78
120
|
}
|
|
79
121
|
|
|
80
|
-
return { lifecycleClientCount, cancel, maybeArm, noteLifecycleClient, startSweep, stopTimers };
|
|
122
|
+
return { lifecycleClientCount, cancel, maybeArm, noteLifecycleClient, noteLifecycleDropped, startSweep, stopTimers };
|
|
81
123
|
}
|
|
@@ -31,6 +31,7 @@ export function createRegistrationReplays({ clients, log }) {
|
|
|
31
31
|
replay.leadPid === identity.leadPid &&
|
|
32
32
|
replay.cwd === identity.cwd &&
|
|
33
33
|
replay.lifecycle === identity.lifecycle &&
|
|
34
|
+
replay.remote === identity.remote &&
|
|
34
35
|
replay.clientKind === identity.clientKind &&
|
|
35
36
|
replay.revision === identity.revision;
|
|
36
37
|
if (sameIdentity && clients.has(replay.token)) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { randomUUID } from 'node:crypto';
|
|
8
8
|
import { parsePid } from '../../runtime/shared/pid-liveness.mjs';
|
|
9
9
|
import { createRegistrationReplays } from './client-registry/registration-replays.mjs';
|
|
10
|
-
import { createClientGrace } from './client-registry/client-grace.mjs';
|
|
10
|
+
import { createClientGrace, clampLeaseMs } from './client-registry/client-grace.mjs';
|
|
11
11
|
|
|
12
12
|
export function createClientRegistry({
|
|
13
13
|
log,
|
|
@@ -18,7 +18,7 @@ export function createClientRegistry({
|
|
|
18
18
|
onClientRegistered,
|
|
19
19
|
onClientDropped,
|
|
20
20
|
}) {
|
|
21
|
-
// token -> { token, leadPid, cwd, lifecycle, sse, pending, lastSeen }
|
|
21
|
+
// token -> { token, leadPid, cwd, lifecycle, remote, leaseMs, sse, pending, lastSeen }
|
|
22
22
|
const clients = new Map();
|
|
23
23
|
let closed = false;
|
|
24
24
|
const replays = createRegistrationReplays({ clients, log });
|
|
@@ -27,6 +27,7 @@ export function createClientRegistry({
|
|
|
27
27
|
log,
|
|
28
28
|
clientGraceMs,
|
|
29
29
|
sweepMs,
|
|
30
|
+
nowMs,
|
|
30
31
|
onClientsEmpty,
|
|
31
32
|
isClosed: () => closed,
|
|
32
33
|
dropClient: (token, reason) => dropClient(token, reason),
|
|
@@ -35,6 +36,7 @@ export function createClientRegistry({
|
|
|
35
36
|
function removeClientRecord(token) {
|
|
36
37
|
const c = clients.get(token);
|
|
37
38
|
if (!c) return;
|
|
39
|
+
if (c.lifecycle) grace.noteLifecycleDropped(c);
|
|
38
40
|
try {
|
|
39
41
|
c.sse?.end?.();
|
|
40
42
|
} catch {}
|
|
@@ -57,6 +59,14 @@ export function createClientRegistry({
|
|
|
57
59
|
grace.maybeArm(reason);
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
/** Refresh a client's lease clock (heartbeat). False for an unknown token. */
|
|
63
|
+
function touch(token) {
|
|
64
|
+
const c = clients.get(token);
|
|
65
|
+
if (!c) return false;
|
|
66
|
+
c.lastSeen = nowMs();
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
60
70
|
function register({
|
|
61
71
|
leadPid,
|
|
62
72
|
cwd,
|
|
@@ -64,11 +74,14 @@ export function createClientRegistry({
|
|
|
64
74
|
clientKind = 'session',
|
|
65
75
|
registrationId = null,
|
|
66
76
|
revision = 0,
|
|
77
|
+
remote = false,
|
|
78
|
+
leaseMs,
|
|
67
79
|
} = {}) {
|
|
68
80
|
const identity = {
|
|
69
81
|
leadPid: parsePid(leadPid) || 0,
|
|
70
82
|
cwd: cwd || null,
|
|
71
83
|
lifecycle: lifecycle !== false,
|
|
84
|
+
remote: remote === true,
|
|
72
85
|
clientKind: clientKind === 'desktop' ? 'desktop' : 'session',
|
|
73
86
|
revision: Math.max(0, Number(revision) || 0),
|
|
74
87
|
};
|
|
@@ -79,6 +92,7 @@ export function createClientRegistry({
|
|
|
79
92
|
clients.set(token, {
|
|
80
93
|
token,
|
|
81
94
|
...identity,
|
|
95
|
+
leaseMs: clampLeaseMs(leaseMs),
|
|
82
96
|
sse: null,
|
|
83
97
|
// True while the socket asked us to stop writing (see writeFrame).
|
|
84
98
|
paused: false,
|
|
@@ -92,7 +106,8 @@ export function createClientRegistry({
|
|
|
92
106
|
grace.startSweep();
|
|
93
107
|
log(
|
|
94
108
|
`client registered token=${token} lead=${identity.leadPid} cwd=${identity.cwd || '-'}` +
|
|
95
|
-
` lifecycle=${identity.lifecycle} kind=${identity.clientKind}`
|
|
109
|
+
` lifecycle=${identity.lifecycle} kind=${identity.clientKind}` +
|
|
110
|
+
(identity.remote ? ` remote leaseMs=${clients.get(token).leaseMs}` : '')
|
|
96
111
|
);
|
|
97
112
|
if (typeof onClientRegistered === 'function') {
|
|
98
113
|
try {
|
|
@@ -102,6 +117,7 @@ export function createClientRegistry({
|
|
|
102
117
|
cwd: identity.cwd,
|
|
103
118
|
lifecycle: identity.lifecycle,
|
|
104
119
|
clientKind: identity.clientKind,
|
|
120
|
+
...(identity.remote ? { remote: true } : {}),
|
|
105
121
|
});
|
|
106
122
|
} catch {}
|
|
107
123
|
}
|
|
@@ -124,6 +140,7 @@ export function createClientRegistry({
|
|
|
124
140
|
forgetReplaysFor: replays.forgetFor,
|
|
125
141
|
dropClient,
|
|
126
142
|
register,
|
|
143
|
+
touch,
|
|
127
144
|
close,
|
|
128
145
|
};
|
|
129
146
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* session-transport/routes.mjs — the HTTP front door: /health (identity
|
|
3
3
|
* travels with every probe), the daemon-token gate, client register /
|
|
4
|
-
* deregister, the /events SSE attach, /call, /shutdown and the /upgrade
|
|
4
|
+
* deregister / heartbeat, the /events SSE attach, /call, /shutdown, /stop and the /upgrade
|
|
5
5
|
* handshake a newer build uses to replace this daemon.
|
|
6
6
|
*/
|
|
7
|
+
import { readFileSync } from 'node:fs';
|
|
7
8
|
import { sendJson, sendError } from '../../runtime/memory/lib/http-wire.mjs';
|
|
9
|
+
import { modelProxyClientMode } from '../../runtime/shared/model-proxy-mode.mjs';
|
|
8
10
|
import {
|
|
9
11
|
compareRuntimeVersions,
|
|
10
12
|
SESSION_CAPABILITY_FINGERPRINT,
|
|
@@ -13,6 +15,32 @@ import {
|
|
|
13
15
|
runtimeVersion,
|
|
14
16
|
} from '../session-wire.mjs';
|
|
15
17
|
|
|
18
|
+
let installBuildHash;
|
|
19
|
+
/** Build hash of a desktop-installed server runtime: the `.install-complete`
|
|
20
|
+
* marker the installer writes at the package root. Null for any other
|
|
21
|
+
* install (npm, source checkout, packaged desktop). */
|
|
22
|
+
function remoteInstallBuildHash() {
|
|
23
|
+
if (installBuildHash !== undefined) return installBuildHash;
|
|
24
|
+
try {
|
|
25
|
+
const marker = JSON.parse(readFileSync(new URL('../../../.install-complete', import.meta.url), 'utf8'));
|
|
26
|
+
installBuildHash = typeof marker?.buildHash === 'string' && marker.buildHash ? marker.buildHash : null;
|
|
27
|
+
} catch {
|
|
28
|
+
installBuildHash = null;
|
|
29
|
+
}
|
|
30
|
+
return installBuildHash;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Turns in flight: what makes a daemon too busy to replace without asking. */
|
|
34
|
+
function busySessionCount(status) {
|
|
35
|
+
return Math.max(0, Number(status?.busy) || 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Leave through the embedder's own graceful SIGTERM path. */
|
|
39
|
+
function requestProcessStop() {
|
|
40
|
+
if (process.listenerCount('SIGTERM') > 0) process.emit('SIGTERM', 'SIGTERM');
|
|
41
|
+
else process.kill(process.pid, 'SIGTERM');
|
|
42
|
+
}
|
|
43
|
+
|
|
16
44
|
export function createTransportRoutes({
|
|
17
45
|
serverToken,
|
|
18
46
|
drain,
|
|
@@ -25,11 +53,23 @@ export function createTransportRoutes({
|
|
|
25
53
|
transportMemory,
|
|
26
54
|
onClientsEmpty,
|
|
27
55
|
onUpgradeRequested,
|
|
56
|
+
admitWork = () => true,
|
|
28
57
|
}) {
|
|
58
|
+
// Checked on every register/call, last right before the work is handed on
|
|
59
|
+
// (after the body is read): a daemon that lost (or cannot re-verify) its
|
|
60
|
+
// owner lease serves nothing.
|
|
61
|
+
const refuseUnadmitted = (res) => {
|
|
62
|
+
if (admitWork()) return false;
|
|
63
|
+
sendError(res, 'daemon owner lease is not held', 503);
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
|
|
29
67
|
const health = (res) => {
|
|
30
68
|
// Identity travels with EVERY health probe: a view negotiates version
|
|
31
69
|
// skew before it attaches, so an embedder can never forget to publish
|
|
32
70
|
// it and leave clients guessing.
|
|
71
|
+
const status = getStatus();
|
|
72
|
+
const busySessions = busySessionCount(status);
|
|
33
73
|
sendJson(res, {
|
|
34
74
|
status: 'ok',
|
|
35
75
|
pid: process.pid,
|
|
@@ -38,10 +78,15 @@ export function createTransportRoutes({
|
|
|
38
78
|
protocol: SESSION_PROTOCOL,
|
|
39
79
|
revision: SESSION_REVISION,
|
|
40
80
|
capabilityFingerprint: SESSION_CAPABILITY_FINGERPRINT,
|
|
81
|
+
modelProxy: modelProxyClientMode() ? 'client' : null,
|
|
41
82
|
draining: drain.reason || null,
|
|
42
83
|
drainCommitted: drain.committed,
|
|
43
84
|
version: runtimeVersion(),
|
|
44
|
-
|
|
85
|
+
packageVersion: runtimeVersion(),
|
|
86
|
+
buildHash: remoteInstallBuildHash(),
|
|
87
|
+
...status,
|
|
88
|
+
busySessions,
|
|
89
|
+
idle: busySessions === 0,
|
|
45
90
|
activeCalls: lanes.active,
|
|
46
91
|
queuedCalls: lanes.queued,
|
|
47
92
|
queuedUrgentCalls: lanes.queuedUrgent,
|
|
@@ -54,6 +99,7 @@ export function createTransportRoutes({
|
|
|
54
99
|
|
|
55
100
|
const register = async (req, res) => {
|
|
56
101
|
const body = await readBody(req);
|
|
102
|
+
if (refuseUnadmitted(res)) return;
|
|
57
103
|
if (drain.committed) {
|
|
58
104
|
sendError(res, `daemon is draining: ${drain.reason}`, 503);
|
|
59
105
|
return;
|
|
@@ -70,6 +116,8 @@ export function createTransportRoutes({
|
|
|
70
116
|
clientKind: body.clientKind,
|
|
71
117
|
registrationId: body.registrationId,
|
|
72
118
|
revision: body.revision,
|
|
119
|
+
remote: body.remote === true,
|
|
120
|
+
leaseMs: body.leaseMs,
|
|
73
121
|
});
|
|
74
122
|
sendJson(res, {
|
|
75
123
|
token: clientToken,
|
|
@@ -85,6 +133,16 @@ export function createTransportRoutes({
|
|
|
85
133
|
sendJson(res, { ok: true });
|
|
86
134
|
};
|
|
87
135
|
|
|
136
|
+
// Lease keep-alive for remote clients (the daemon token gate already ran).
|
|
137
|
+
const heartbeat = async (req, res) => {
|
|
138
|
+
const body = await readBody(req);
|
|
139
|
+
if (!body.token || !registry.touch(String(body.token))) {
|
|
140
|
+
sendError(res, 'unknown client token', 404);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
sendJson(res, { ok: true });
|
|
144
|
+
};
|
|
145
|
+
|
|
88
146
|
const events = (url, res) => {
|
|
89
147
|
const clientToken = url.searchParams.get('token');
|
|
90
148
|
if (!attachSse(clientToken, res)) sendError(res, 'unknown client token', 404);
|
|
@@ -92,11 +150,15 @@ export function createTransportRoutes({
|
|
|
92
150
|
};
|
|
93
151
|
|
|
94
152
|
const call = async (req, res) => {
|
|
153
|
+
if (refuseUnadmitted(res)) return;
|
|
95
154
|
if (drain.committed) {
|
|
96
155
|
sendError(res, `daemon is draining: ${drain.reason}`, 503);
|
|
97
156
|
return;
|
|
98
157
|
}
|
|
99
158
|
const body = await readBody(req);
|
|
159
|
+
// Again right before dispatch: the lease may have been fenced while the
|
|
160
|
+
// body was still arriving.
|
|
161
|
+
if (refuseUnadmitted(res)) return;
|
|
100
162
|
await callRoute(body, res);
|
|
101
163
|
};
|
|
102
164
|
|
|
@@ -109,6 +171,19 @@ export function createTransportRoutes({
|
|
|
109
171
|
}
|
|
110
172
|
};
|
|
111
173
|
|
|
174
|
+
// Authenticated stop for a desktop replacing this daemon with another
|
|
175
|
+
// build: refused while turns run unless forced (the user confirmed).
|
|
176
|
+
const stop = async (req, res) => {
|
|
177
|
+
const body = await readBody(req);
|
|
178
|
+
const busySessions = busySessionCount(getStatus());
|
|
179
|
+
if (busySessions > 0 && body.force !== true) {
|
|
180
|
+
sendError(res, `daemon is busy: ${busySessions} running session(s)`, 409);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
sendJson(res, { ok: true, pid: process.pid, busySessions });
|
|
184
|
+
setImmediate(requestProcessStop);
|
|
185
|
+
};
|
|
186
|
+
|
|
112
187
|
const upgrade = async (req, res) => {
|
|
113
188
|
const body = await readBody(req);
|
|
114
189
|
const requestedProtocol = Number(body.protocol);
|
|
@@ -150,8 +225,10 @@ export function createTransportRoutes({
|
|
|
150
225
|
const tokenRoutes = {
|
|
151
226
|
'POST /client/register': register,
|
|
152
227
|
'POST /client/deregister': deregister,
|
|
228
|
+
'POST /client/heartbeat': heartbeat,
|
|
153
229
|
'POST /call': call,
|
|
154
230
|
'POST /shutdown': (_req, res) => shutdown(res),
|
|
231
|
+
'POST /stop': stop,
|
|
155
232
|
'POST /upgrade': upgrade,
|
|
156
233
|
};
|
|
157
234
|
|
|
@@ -76,6 +76,7 @@ export function createTransportStack({
|
|
|
76
76
|
onClientDropped,
|
|
77
77
|
onUpgradeRequested,
|
|
78
78
|
getStatus,
|
|
79
|
+
admitWork,
|
|
79
80
|
}) {
|
|
80
81
|
const body = createBodyReader();
|
|
81
82
|
const callCache = createSessionCallCache({
|
|
@@ -125,6 +126,7 @@ export function createTransportStack({
|
|
|
125
126
|
}),
|
|
126
127
|
onClientsEmpty,
|
|
127
128
|
onUpgradeRequested,
|
|
129
|
+
admitWork,
|
|
128
130
|
});
|
|
129
131
|
|
|
130
132
|
return { registry, clients, lanes, callCache, broadcast, handleRequest };
|
|
@@ -32,6 +32,9 @@ export function createSessionTransport({
|
|
|
32
32
|
onClientDropped = null,
|
|
33
33
|
onUpgradeRequested = null,
|
|
34
34
|
getStatus = () => ({}),
|
|
35
|
+
// Asked before every register/call; false answers 503 (the daemon's owner
|
|
36
|
+
// lease is not held — daemon-owner.mjs).
|
|
37
|
+
admitWork = () => true,
|
|
35
38
|
} = {}) {
|
|
36
39
|
if (typeof handleCall !== 'function') throw new Error('handleCall is required');
|
|
37
40
|
|
|
@@ -52,6 +55,7 @@ export function createSessionTransport({
|
|
|
52
55
|
onClientDropped,
|
|
53
56
|
onUpgradeRequested,
|
|
54
57
|
getStatus,
|
|
58
|
+
admitWork,
|
|
55
59
|
});
|
|
56
60
|
|
|
57
61
|
function beginDrain(reason = 'daemon replacement') {
|
|
@@ -69,10 +73,19 @@ export function createSessionTransport({
|
|
|
69
73
|
return true;
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
// MIXDOG_SESSION_PORT pins the port (an SSH forward needs a stable one).
|
|
77
|
+
function configuredPort() {
|
|
78
|
+
const port = Number(process.env.MIXDOG_SESSION_PORT);
|
|
79
|
+
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
function start() {
|
|
73
83
|
if (closed) return Promise.reject(new Error('session service transport is closed'));
|
|
84
|
+
const fixedPort = configuredPort();
|
|
74
85
|
listener ??= createLoopbackListener({
|
|
75
86
|
server: http.createServer(handleRequest),
|
|
87
|
+
basePort: fixedPort,
|
|
88
|
+
maxPort: fixedPort,
|
|
76
89
|
onListening(port) {
|
|
77
90
|
boundPort = port;
|
|
78
91
|
log(`session service transport listening on 127.0.0.1:${boundPort} pid=${process.pid}`);
|
|
@@ -81,7 +94,17 @@ export function createSessionTransport({
|
|
|
81
94
|
if (!fatal) log(`server error: ${error?.message || error}`);
|
|
82
95
|
},
|
|
83
96
|
});
|
|
84
|
-
return listener.start().then((port) =>
|
|
97
|
+
return listener.start().then(async (port) => {
|
|
98
|
+
// The listener falls back to an OS-chosen port when the range is busy;
|
|
99
|
+
// a pinned port must never silently become a different one.
|
|
100
|
+
if (fixedPort && port !== fixedPort) {
|
|
101
|
+
await listener.stop();
|
|
102
|
+
listener = null;
|
|
103
|
+
boundPort = null;
|
|
104
|
+
throw new Error(`session port ${fixedPort} (MIXDOG_SESSION_PORT) is unavailable`);
|
|
105
|
+
}
|
|
106
|
+
return { port, token: serverToken };
|
|
107
|
+
});
|
|
85
108
|
}
|
|
86
109
|
|
|
87
110
|
async function stop() {
|
|
@@ -18,7 +18,8 @@ function selectableUserItems(items, limit = MAX_SELECTABLE_MESSAGES) {
|
|
|
18
18
|
for (let index = source.length - 1; index >= 0 && rows.length < limit; index -= 1) {
|
|
19
19
|
const item = source[index];
|
|
20
20
|
if (item?.kind !== 'user') continue;
|
|
21
|
-
|
|
21
|
+
// Prompts a compaction archived are display-only.
|
|
22
|
+
if (item?.id == null || item.archived === true) continue;
|
|
22
23
|
const text = String(item.text || '').trim();
|
|
23
24
|
if (!text) continue;
|
|
24
25
|
rows.push({ id: String(item.id), text });
|
|
@@ -191,6 +191,34 @@ const PROVIDER_DISPLAY_NAMES = new Map([
|
|
|
191
191
|
export const providerDisplayName = (provider) =>
|
|
192
192
|
PROVIDER_DISPLAY_NAMES.get(String(provider || '').toLowerCase()) ?? (provider || 'Provider');
|
|
193
193
|
|
|
194
|
+
export const resolveProviderDisplayName = (provider, providerNames = null) =>
|
|
195
|
+
(String(provider || '')
|
|
196
|
+
.trim()
|
|
197
|
+
.startsWith('custom-') &&
|
|
198
|
+
providerNames?.[String(provider).trim()]) ||
|
|
199
|
+
providerDisplayName(provider);
|
|
200
|
+
|
|
201
|
+
export const loadProviderDisplayNames = async (store) => {
|
|
202
|
+
let setup = null;
|
|
203
|
+
try {
|
|
204
|
+
setup = await store?.getProviderSetup?.();
|
|
205
|
+
} catch {
|
|
206
|
+
return {};
|
|
207
|
+
}
|
|
208
|
+
const names = {};
|
|
209
|
+
for (const group of ['api', 'oauth', 'local']) {
|
|
210
|
+
const entries = setup?.[group];
|
|
211
|
+
if (!Array.isArray(entries)) continue;
|
|
212
|
+
for (const entry of entries) {
|
|
213
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
214
|
+
const id = String(entry.id || entry.provider || '').trim();
|
|
215
|
+
const name = String(entry.name || entry.label || '').trim();
|
|
216
|
+
if (id && name) names[id] = name;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return names;
|
|
220
|
+
};
|
|
221
|
+
|
|
194
222
|
const PROVIDER_DISPLAY_RANKS = {
|
|
195
223
|
default: 0,
|
|
196
224
|
'openai-oauth': 10,
|
|
@@ -247,7 +275,7 @@ const routeModelDisplayName = (route) =>
|
|
|
247
275
|
.filter(Boolean)
|
|
248
276
|
.join(' ');
|
|
249
277
|
|
|
250
|
-
const groupModelsByProvider = (models) => {
|
|
278
|
+
const groupModelsByProvider = (models, providerNames = null) => {
|
|
251
279
|
const providers = new Map();
|
|
252
280
|
for (const model of models) {
|
|
253
281
|
if (!providers.has(model.provider)) providers.set(model.provider, []);
|
|
@@ -257,22 +285,24 @@ const groupModelsByProvider = (models) => {
|
|
|
257
285
|
const orderedProviders = [...providers.keys()].sort((a, b) => {
|
|
258
286
|
const rank = providerDisplayRank(a) - providerDisplayRank(b);
|
|
259
287
|
if (rank !== 0) return rank;
|
|
260
|
-
const label =
|
|
288
|
+
const label = resolveProviderDisplayName(a, providerNames).localeCompare(
|
|
289
|
+
resolveProviderDisplayName(b, providerNames)
|
|
290
|
+
);
|
|
261
291
|
if (label !== 0) return label;
|
|
262
292
|
return a.localeCompare(b);
|
|
263
293
|
});
|
|
264
294
|
return { providers, orderedProviders };
|
|
265
295
|
};
|
|
266
296
|
|
|
267
|
-
export const buildModelProviderItems = (models, currentRoute = null) => {
|
|
268
|
-
const { providers, orderedProviders } = groupModelsByProvider(models);
|
|
297
|
+
export const buildModelProviderItems = (models, currentRoute = null, providerNames = null) => {
|
|
298
|
+
const { providers, orderedProviders } = groupModelsByProvider(models, providerNames);
|
|
269
299
|
return orderedProviders.map((provider) => {
|
|
270
300
|
const providerModels = providers.get(provider) || [];
|
|
271
301
|
const currentModel =
|
|
272
302
|
currentRoute?.provider === provider ? providerModels.find((model) => model.id === currentRoute.model) : null;
|
|
273
303
|
return {
|
|
274
304
|
value: `provider:${provider}`,
|
|
275
|
-
label:
|
|
305
|
+
label: resolveProviderDisplayName(provider, providerNames),
|
|
276
306
|
marker: currentModel ? '✓' : '',
|
|
277
307
|
markerColor: theme.success,
|
|
278
308
|
meta: currentModel ? modelRecordDisplayName(currentModel) : '',
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// One provider's model list: rows from the catalog, the footer for the
|
|
3
3
|
// highlighted row, and the keys that change its selections (←/→ effort,
|
|
4
4
|
// C/Shift+C context, Tab Fast, T thinking). Enter saves through the caller.
|
|
5
|
-
import { buildProviderModelItems,
|
|
5
|
+
import { buildProviderModelItems, resolveProviderDisplayName } from '../model-options.mjs';
|
|
6
6
|
import { modelFooter } from './model-footer.mjs';
|
|
7
7
|
import { createRouteSelection } from './route-selection.mjs';
|
|
8
8
|
|
|
@@ -73,7 +73,7 @@ export function openProviderModelsPicker({ view, provider, onBack, saveRoute })
|
|
|
73
73
|
)
|
|
74
74
|
);
|
|
75
75
|
view.paint({
|
|
76
|
-
title:
|
|
76
|
+
title: resolveProviderDisplayName(provider, view.providerNames),
|
|
77
77
|
description: options.modelDescription || 'Select a model. Adjust Effort with ←/→.',
|
|
78
78
|
footer: (item) => modelFooter(selection, item?._model),
|
|
79
79
|
help:
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* state (getState) is read through a getter so it always reflects the current
|
|
11
11
|
* render.
|
|
12
12
|
*/
|
|
13
|
-
import { normalizeModelOptions, buildModelProviderItems } from './model-options.mjs';
|
|
13
|
+
import { normalizeModelOptions, buildModelProviderItems, loadProviderDisplayNames } from './model-options.mjs';
|
|
14
14
|
import { createModelCatalog } from './model-picker/catalog-load.mjs';
|
|
15
15
|
import { openProviderModelsPicker, saveSelectedRoute } from './model-picker/provider-models-list.mjs';
|
|
16
16
|
|
|
@@ -77,6 +77,7 @@ export function createModelPicker({
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const models = normalizeModelOptions(providerModels);
|
|
80
|
+
const providerNames = await loadProviderDisplayNames(store);
|
|
80
81
|
const activeRoute = options.currentRoute || {
|
|
81
82
|
provider: state.provider,
|
|
82
83
|
model: state.model,
|
|
@@ -90,6 +91,7 @@ export function createModelPicker({
|
|
|
90
91
|
state,
|
|
91
92
|
models,
|
|
92
93
|
activeRoute,
|
|
94
|
+
providerNames,
|
|
93
95
|
returnTo,
|
|
94
96
|
returnLabel,
|
|
95
97
|
returnOnNestedCancel,
|
|
@@ -114,7 +116,7 @@ export function createModelPicker({
|
|
|
114
116
|
providerListHighlightProvider = renderOptions.highlightProvider;
|
|
115
117
|
}
|
|
116
118
|
const highlightProvider = renderOptions.highlightProvider || providerListHighlightProvider || null;
|
|
117
|
-
const providerItems = buildModelProviderItems(models, activeRoute);
|
|
119
|
+
const providerItems = buildModelProviderItems(models, activeRoute, providerNames);
|
|
118
120
|
const providerHighlight = highlightProvider || activeRoute?.provider || null;
|
|
119
121
|
const providerInitialIndex = Math.max(
|
|
120
122
|
0,
|