mixdog 0.9.151 → 0.9.152
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 +110 -68
- package/package.json +3 -2
- package/scripts/tool-stress.mjs +0 -2
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +3 -10
- package/src/headless-exec.test.mjs +2 -7
- package/src/runtime/agent/orchestrator/config.mjs +6 -66
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/browser-bridge/client.mjs +103 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
- package/src/runtime/computer-bridge/client.mjs +101 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +1 -1
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +2 -1
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-surface.mjs +18 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/goal-runtime.mjs +721 -0
- package/src/session-runtime/goal-runtime.test.mjs +108 -0
- package/src/session-runtime/lifecycle-api.mjs +18 -4
- package/src/session-runtime/lifecycle-api.test.mjs +21 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/resource-api-global.test.mjs +69 -0
- package/src/session-runtime/resource-api.mjs +173 -58
- package/src/session-runtime/runtime-core.mjs +148 -20
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-turn-api.mjs +22 -11
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +4 -13
- package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
- package/src/session-runtime/tool-surface.mjs +12 -0
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/notify.mjs +6 -8
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/daemon.mjs +31 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +696 -58
- package/src/standalone/session-runtime-inline-host.mjs +351 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +476 -18
- package/src/standalone/session-service.mjs +81 -6
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +163 -44
- package/src/tui/session/context-state.mjs +1 -0
- package/src/tui/session/goal-continuation.mjs +110 -0
- package/src/tui/session/goal-continuation.test.mjs +84 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +108 -0
- package/src/tui/session/session-api-ext.mjs +39 -8
- package/src/tui/session/session-api.mjs +58 -0
- package/src/tui/session/session-flow.mjs +6 -0
- package/src/tui/session/turn.mjs +1 -0
- package/src/tui/session-local.mjs +15 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
|
@@ -97,6 +97,7 @@ export function createSessionService({
|
|
|
97
97
|
createSessionRuntime = null,
|
|
98
98
|
sessionExists = null,
|
|
99
99
|
readStoredSession = null,
|
|
100
|
+
subscribeExternalSessionStates = null,
|
|
100
101
|
listSessions = null,
|
|
101
102
|
getRemoteSessionState = null,
|
|
102
103
|
desktopRuntime = null,
|
|
@@ -138,7 +139,7 @@ export function createSessionService({
|
|
|
138
139
|
// destroying anything, this sweep is the ONLY reclaim path besides shutdown.
|
|
139
140
|
const IDLE_EVICT_MS = Number(idleEvictMs) > 0
|
|
140
141
|
? Number(idleEvictMs)
|
|
141
|
-
: Math.max(60_000, Number(process.env.MIXDOG_SESSION_IDLE_EVICT_MS) ||
|
|
142
|
+
: Math.max(60_000, Number(process.env.MIXDOG_SESSION_IDLE_EVICT_MS) || 5 * 60_000);
|
|
142
143
|
const EVICT_SWEEP_MS = Number(evictSweepMs) > 0 ? Number(evictSweepMs) : 30_000;
|
|
143
144
|
let evictTimer = null;
|
|
144
145
|
|
|
@@ -170,6 +171,7 @@ export function createSessionService({
|
|
|
170
171
|
// the entry the moment one materializes, so its live frames start flowing
|
|
171
172
|
// without a second subscribe round-trip.
|
|
172
173
|
const pendingViewers = new Map(); // sessionId -> Set<clientToken>
|
|
174
|
+
const externalViewEntries = new Map(); // sessionId -> ordinary projected entry
|
|
173
175
|
|
|
174
176
|
function trackPendingViewer(sessionId, ctx) {
|
|
175
177
|
const token = subscriberToken(ctx);
|
|
@@ -216,6 +218,10 @@ export function createSessionService({
|
|
|
216
218
|
startEvictionSweep();
|
|
217
219
|
log(`session ${currentSessionId(entry) || '(creating)'} unwatched (client ${token} gone) — retained`);
|
|
218
220
|
}
|
|
221
|
+
for (const [sessionId, entry] of [...externalViewEntries]) {
|
|
222
|
+
entry.subscribers?.delete(token);
|
|
223
|
+
if ((entry.subscribers?.size || 0) === 0) externalViewEntries.delete(sessionId);
|
|
224
|
+
}
|
|
219
225
|
for (const service of desktopServicesById.values()) {
|
|
220
226
|
service.subscribers.delete(token);
|
|
221
227
|
}
|
|
@@ -428,11 +434,22 @@ export function createSessionService({
|
|
|
428
434
|
if (entry) entry.indexedSessionId = '';
|
|
429
435
|
return '';
|
|
430
436
|
}
|
|
437
|
+
// External agent projections use the same frame machinery but are not a
|
|
438
|
+
// daemon execution owner. Keeping them out of sessionsById lets a later
|
|
439
|
+
// ordinary session materialization adopt the viewers and take authority.
|
|
440
|
+
if (entry.externalView === true) return nextId;
|
|
431
441
|
const existing = sessionsById.get(nextId);
|
|
432
442
|
if (existing && existing !== entry && !existing.disposed) {
|
|
433
443
|
// Do not redirect an established address to a second session runtime. The load
|
|
434
444
|
throw new Error(`duplicate session address: ${nextId}`);
|
|
435
445
|
}
|
|
446
|
+
const external = externalViewEntries.get(nextId);
|
|
447
|
+
if (external) {
|
|
448
|
+
externalViewEntries.delete(nextId);
|
|
449
|
+
for (const token of external.subscribers || []) {
|
|
450
|
+
addSubscriber(entry, { clientToken: token });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
436
453
|
sessionsById.set(nextId, entry);
|
|
437
454
|
entry.indexedSessionId = nextId;
|
|
438
455
|
return nextId;
|
|
@@ -460,6 +477,54 @@ export function createSessionService({
|
|
|
460
477
|
}, entry.subscribers);
|
|
461
478
|
}
|
|
462
479
|
|
|
480
|
+
function externalEntryForView(sessionId) {
|
|
481
|
+
return externalViewEntries.get(String(sessionId || '')) || null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function publishExternalSessionState(update) {
|
|
485
|
+
const sessionId = String(update?.sessionId || '');
|
|
486
|
+
const snapshot = update?.snapshot;
|
|
487
|
+
if (!sessionId || !snapshot || typeof snapshot !== 'object') return;
|
|
488
|
+
// A daemon-owned runtime is the canonical owner if this address was
|
|
489
|
+
// materialized. External agent projection frames can arrive one tick late
|
|
490
|
+
// after that promotion and must not overwrite it.
|
|
491
|
+
if (sessionOwner(sessionId)) return;
|
|
492
|
+
let entry = externalViewEntries.get(sessionId);
|
|
493
|
+
if (!entry) {
|
|
494
|
+
if (!pendingViewers.has(sessionId)) return;
|
|
495
|
+
let state = { ...snapshot, sessionId };
|
|
496
|
+
const runtime = {
|
|
497
|
+
isWireSafe: true,
|
|
498
|
+
getState: () => state,
|
|
499
|
+
setState: (next) => { state = next; },
|
|
500
|
+
};
|
|
501
|
+
entry = {
|
|
502
|
+
runtime,
|
|
503
|
+
subscribers: new Set(),
|
|
504
|
+
disposed: false,
|
|
505
|
+
timer: null,
|
|
506
|
+
lastPublishedAt: 0,
|
|
507
|
+
publishedSessionId: '',
|
|
508
|
+
indexedSessionId: '',
|
|
509
|
+
addressedSessionId: sessionId,
|
|
510
|
+
revision: revisionEpoch,
|
|
511
|
+
busy: null,
|
|
512
|
+
externalView: true,
|
|
513
|
+
};
|
|
514
|
+
externalViewEntries.set(sessionId, entry);
|
|
515
|
+
adoptPendingViewers(entry, sessionId);
|
|
516
|
+
} else {
|
|
517
|
+
entry.runtime.setState({ ...snapshot, sessionId });
|
|
518
|
+
}
|
|
519
|
+
const step = advance(entry);
|
|
520
|
+
if (step.changed) publishStep(entry, step);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const unsubscribeExternalSessionStates =
|
|
524
|
+
typeof subscribeExternalSessionStates === 'function'
|
|
525
|
+
? subscribeExternalSessionStates(publishExternalSessionState)
|
|
526
|
+
: () => {};
|
|
527
|
+
|
|
463
528
|
/** Response body for the CALLER, which announced the revision it holds. */
|
|
464
529
|
function bodyForClient(step, baseRevision) {
|
|
465
530
|
if (!step.changed && baseRevision === step.revision) return { revision: step.revision };
|
|
@@ -911,7 +976,7 @@ export function createSessionService({
|
|
|
911
976
|
}
|
|
912
977
|
const id = String(sessionId || '');
|
|
913
978
|
if (!id) throw new TypeError('sessionId is required');
|
|
914
|
-
const live = await liveEntryForView(id);
|
|
979
|
+
const live = await liveEntryForView(id) || externalEntryForView(id);
|
|
915
980
|
if (live) {
|
|
916
981
|
const step = advance(live);
|
|
917
982
|
retainUnwatched(live, 'headless session read');
|
|
@@ -922,7 +987,7 @@ export function createSessionService({
|
|
|
922
987
|
}
|
|
923
988
|
if (typeof readStoredSession === 'function') {
|
|
924
989
|
const projection = await storedSessionProjection(id, openHints);
|
|
925
|
-
const lateOwner = sessionOwner(id);
|
|
990
|
+
const lateOwner = sessionOwner(id) || externalEntryForView(id);
|
|
926
991
|
if (lateOwner) {
|
|
927
992
|
const step = advance(lateOwner);
|
|
928
993
|
retainUnwatched(lateOwner, 'headless session read');
|
|
@@ -952,7 +1017,7 @@ export function createSessionService({
|
|
|
952
1017
|
) {
|
|
953
1018
|
const id = String(sessionId || '');
|
|
954
1019
|
if (!id) throw new TypeError('sessionId is required');
|
|
955
|
-
const live = await liveEntryForView(id);
|
|
1020
|
+
const live = await liveEntryForView(id) || externalEntryForView(id);
|
|
956
1021
|
if (live) {
|
|
957
1022
|
addSubscriber(live, ctx);
|
|
958
1023
|
const step = advance(live);
|
|
@@ -964,7 +1029,7 @@ export function createSessionService({
|
|
|
964
1029
|
// guarantees either adoption or the live re-check below.
|
|
965
1030
|
trackPendingViewer(id, ctx);
|
|
966
1031
|
const projection = await storedSessionProjection(id, openHints);
|
|
967
|
-
const lateOwner = sessionOwner(id);
|
|
1032
|
+
const lateOwner = sessionOwner(id) || externalEntryForView(id);
|
|
968
1033
|
if (lateOwner) {
|
|
969
1034
|
dropPendingViewer(id, ctx);
|
|
970
1035
|
addSubscriber(lateOwner, ctx);
|
|
@@ -988,7 +1053,15 @@ export function createSessionService({
|
|
|
988
1053
|
if (!id) throw new TypeError('sessionId is required');
|
|
989
1054
|
dropPendingViewer(id, ctx);
|
|
990
1055
|
const owner = sessionOwner(id);
|
|
991
|
-
if (!owner)
|
|
1056
|
+
if (!owner) {
|
|
1057
|
+
const external = externalEntryForView(id);
|
|
1058
|
+
if (external) {
|
|
1059
|
+
const token = subscriberToken(ctx);
|
|
1060
|
+
if (token) external.subscribers?.delete(token);
|
|
1061
|
+
if ((external.subscribers?.size || 0) === 0) externalViewEntries.delete(id);
|
|
1062
|
+
}
|
|
1063
|
+
return { sessionId: id, unsubscribed: true };
|
|
1064
|
+
}
|
|
992
1065
|
const entry = owner;
|
|
993
1066
|
const token = subscriberToken(ctx);
|
|
994
1067
|
if (token) entry.subscribers?.delete(token);
|
|
@@ -1280,6 +1353,8 @@ export function createSessionService({
|
|
|
1280
1353
|
|
|
1281
1354
|
async function stop(reason = 'service stop') {
|
|
1282
1355
|
closed = true;
|
|
1356
|
+
try { unsubscribeExternalSessionStates(); } catch {}
|
|
1357
|
+
externalViewEntries.clear();
|
|
1283
1358
|
if (evictTimer) { clearInterval(evictTimer); evictTimer = null; }
|
|
1284
1359
|
const services = [...new Set(desktopServicesById.values())];
|
|
1285
1360
|
desktopServicesById.clear();
|
package/src/tui/app/app-view.jsx
CHANGED
|
@@ -301,6 +301,7 @@ export function renderAppView(ctx) {
|
|
|
301
301
|
indexMode={picker.indexMode}
|
|
302
302
|
visibleCount={pickerVisibleRows}
|
|
303
303
|
fillHeight={expandedOptionPanel}
|
|
304
|
+
loading={picker.loading === true}
|
|
304
305
|
themeEpoch={state.themeEpoch || 0}
|
|
305
306
|
confirmBar={picker.confirmBar}
|
|
306
307
|
/>
|
|
@@ -39,6 +39,7 @@ export function createCoreMemoryPicker({
|
|
|
39
39
|
// Loading state lives in the header description row, not as a fake
|
|
40
40
|
// selectable menu item.
|
|
41
41
|
description: 'Loading memories…',
|
|
42
|
+
loading: true,
|
|
42
43
|
items: [],
|
|
43
44
|
onSelect: () => {},
|
|
44
45
|
onCancel: closeMemoryCorePicker,
|
|
@@ -113,6 +114,7 @@ export function createCoreMemoryPicker({
|
|
|
113
114
|
paintPanel({
|
|
114
115
|
title: 'Memory · List',
|
|
115
116
|
description: 'Loading memories…',
|
|
117
|
+
loading: true,
|
|
116
118
|
items: [],
|
|
117
119
|
onSelect: () => {},
|
|
118
120
|
onCancel: () => openMemoryCorePicker(),
|
|
@@ -158,7 +158,7 @@ export function createMaintenancePickers({
|
|
|
158
158
|
// binds it to the previous owner's epoch, which the open transition (a
|
|
159
159
|
// panel identity change) supersedes — leaving "Latest version" stuck on
|
|
160
160
|
// "checking…". Esc after the paint still closes the panel for good.
|
|
161
|
-
|
|
161
|
+
return Promise.resolve(render({ checking: true }))
|
|
162
162
|
.catch((e) => {
|
|
163
163
|
store.pushNotice(`update panel failed: ${e?.message || e}`, 'error');
|
|
164
164
|
return false;
|
|
@@ -308,8 +308,8 @@ export function createMaintenancePickers({
|
|
|
308
308
|
setSettingsPrompt(null);
|
|
309
309
|
own.context(null);
|
|
310
310
|
closeUsagePanel();
|
|
311
|
-
|
|
312
|
-
|
|
311
|
+
return Promise.resolve(options.advanced === true ? renderAdvanced() : render())
|
|
312
|
+
.catch((e) => store.pushNotice(`auto-clear panel failed: ${e?.message || e}`, 'error'));
|
|
313
313
|
};
|
|
314
314
|
|
|
315
315
|
const openProfilePicker = async (options = {}) => {
|
|
@@ -166,6 +166,7 @@ export const providerDisplayName = (provider) => {
|
|
|
166
166
|
if (key === 'xai' || key === 'xai-api') return 'xAI API';
|
|
167
167
|
if (key === 'deepseek' || key === 'deepseek-api') return 'DeepSeek API';
|
|
168
168
|
if (key === 'opencode-go') return 'OpenCode Go API';
|
|
169
|
+
if (key === 'openrouter') return 'OpenRouter';
|
|
169
170
|
if (key === 'ollama') return 'Ollama';
|
|
170
171
|
if (key === 'lmstudio') return 'LM Studio';
|
|
171
172
|
if (key === 'default') return 'Default';
|
|
@@ -182,6 +183,7 @@ export const providerDisplayRank = (provider) => {
|
|
|
182
183
|
'antigravity-oauth': 31,
|
|
183
184
|
'cursor-oauth': 32,
|
|
184
185
|
'opencode-go': 35,
|
|
186
|
+
openrouter: 36,
|
|
185
187
|
openai: 40,
|
|
186
188
|
'openai-api': 40,
|
|
187
189
|
anthropic: 50,
|
|
@@ -59,6 +59,9 @@ export function createModelPicker({
|
|
|
59
59
|
const returnTo = typeof options.returnTo === 'function' ? options.returnTo : null;
|
|
60
60
|
const returnLabel = String(options.returnLabel || 'Agents');
|
|
61
61
|
const returnOnNestedCancel = options.returnOnNestedCancel === true;
|
|
62
|
+
const handoffPanel = options.handoffPanel && typeof options.handoffPanel === 'object'
|
|
63
|
+
? options.handoffPanel
|
|
64
|
+
: null;
|
|
62
65
|
const cancelModelPicker = () => {
|
|
63
66
|
modelPickerClosed = true;
|
|
64
67
|
if (returnTo) returnTo();
|
|
@@ -77,6 +80,7 @@ export function createModelPicker({
|
|
|
77
80
|
title: options.title || 'Model',
|
|
78
81
|
description: options.loadingDescription || 'Loading models...',
|
|
79
82
|
help: returnTo ? `↑/↓ Select · Enter Open · Esc ${returnLabel}` : '↑/↓ Select · Enter Open · Esc Back',
|
|
83
|
+
loading: true,
|
|
80
84
|
items: [],
|
|
81
85
|
onCancel: cancelModelPicker,
|
|
82
86
|
});
|
|
@@ -382,7 +386,7 @@ export function createModelPicker({
|
|
|
382
386
|
if (typeof options.onImmediateSelect === 'function') {
|
|
383
387
|
options.onImmediateSelect(routeInput, selected, effort);
|
|
384
388
|
} else {
|
|
385
|
-
own.paint(
|
|
389
|
+
own.paint(handoffPanel);
|
|
386
390
|
}
|
|
387
391
|
// Post-ack delegation (return to the caller's panel), bound to the
|
|
388
392
|
// claim AFTER this keypress's own navigation: an Esc between the
|
|
@@ -397,10 +401,13 @@ export function createModelPicker({
|
|
|
397
401
|
afterSelect();
|
|
398
402
|
return result;
|
|
399
403
|
})
|
|
400
|
-
.catch((e) =>
|
|
404
|
+
.catch((e) => {
|
|
405
|
+
store.pushNotice(`Couldn’t save model: ${e?.message || e}`, 'error');
|
|
406
|
+
if (handoffPanel) afterSelect();
|
|
407
|
+
});
|
|
401
408
|
return;
|
|
402
409
|
}
|
|
403
|
-
own.paint(
|
|
410
|
+
own.paint(handoffPanel);
|
|
404
411
|
const afterSelect = own.defer(() => {
|
|
405
412
|
if (typeof options.onAfterSelect === 'function') options.onAfterSelect();
|
|
406
413
|
});
|
|
@@ -415,7 +422,10 @@ export function createModelPicker({
|
|
|
415
422
|
);
|
|
416
423
|
if (ok) afterSelect();
|
|
417
424
|
})
|
|
418
|
-
.catch((e) =>
|
|
425
|
+
.catch((e) => {
|
|
426
|
+
store.pushNotice(`Couldn’t switch model: ${e?.message || e}`, 'error');
|
|
427
|
+
if (handoffPanel) afterSelect();
|
|
428
|
+
});
|
|
419
429
|
};
|
|
420
430
|
const renderProviderModels = () => {
|
|
421
431
|
const providerModelItems = buildProviderModelItems(models, provider, activeRoute);
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { createModelPicker } from './model-picker.mjs';
|
|
5
|
+
import { shouldSupersedePanelEpoch, supersedePanelEpoch } from './panel-epoch.mjs';
|
|
6
|
+
import { createPanelSurface } from './panel-surface.mjs';
|
|
7
|
+
import { createRoutePickers } from './route-pickers.mjs';
|
|
8
|
+
import { createSettingsPicker } from './settings-picker.mjs';
|
|
9
|
+
import { createSlashDispatch } from './slash-dispatch.mjs';
|
|
10
|
+
import { createThemeEffortPickers } from './theme-effort-pickers.mjs';
|
|
11
|
+
|
|
12
|
+
function deferred() {
|
|
13
|
+
let resolve;
|
|
14
|
+
let reject;
|
|
15
|
+
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
|
|
16
|
+
return { promise, resolve, reject };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const flush = async () => {
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
21
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function createPanelHost() {
|
|
25
|
+
let current = null;
|
|
26
|
+
const painted = [];
|
|
27
|
+
const setPicker = (next) => {
|
|
28
|
+
const previous = current;
|
|
29
|
+
current = typeof next === 'function' ? next(previous) : next;
|
|
30
|
+
if (shouldSupersedePanelEpoch(previous, current)) supersedePanelEpoch();
|
|
31
|
+
painted.push(current);
|
|
32
|
+
};
|
|
33
|
+
const surface = createPanelSurface({
|
|
34
|
+
setPicker,
|
|
35
|
+
setContextPanel: () => {},
|
|
36
|
+
setUsagePanel: () => {},
|
|
37
|
+
});
|
|
38
|
+
return { current: () => current, painted, setPicker, surface };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const HANDOFF = {
|
|
42
|
+
title: 'Settings',
|
|
43
|
+
description: 'Applying selection...',
|
|
44
|
+
help: 'Esc Close',
|
|
45
|
+
indexMode: 'never',
|
|
46
|
+
pickerKey: 'settings-handoff',
|
|
47
|
+
loading: true,
|
|
48
|
+
items: [],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function settingsDeps(host, store, opened = {}) {
|
|
52
|
+
const noop = () => {};
|
|
53
|
+
return {
|
|
54
|
+
store: { pushNotice: noop, ...store },
|
|
55
|
+
state: { model: 'gpt-5', provider: 'openai', workflow: { id: 'default' } },
|
|
56
|
+
surface: host.surface,
|
|
57
|
+
setProviderPrompt: noop,
|
|
58
|
+
setSettingsPrompt: noop,
|
|
59
|
+
settingsHeavyCacheRef: { current: null },
|
|
60
|
+
settingsRequestRef: { current: 0 },
|
|
61
|
+
formatDuration: (ms) => `${ms}ms`,
|
|
62
|
+
displayModelName: () => 'GPT-5',
|
|
63
|
+
routeModelLabel: () => '(unset)',
|
|
64
|
+
workflowDisplayName: () => 'Default',
|
|
65
|
+
workflowSwitchNotice: () => 'workflow switched',
|
|
66
|
+
themeNotice: () => 'theme set',
|
|
67
|
+
openModelPicker: (options) => { opened.model = options; },
|
|
68
|
+
openWebSearchPicker: noop,
|
|
69
|
+
openAgentsPicker: noop,
|
|
70
|
+
openWorkflowPicker: (options) => { opened.workflow = options; },
|
|
71
|
+
openOutputStylePicker: (options) => { opened.outputStyle = options; },
|
|
72
|
+
openProviderSetupPicker: noop,
|
|
73
|
+
openThemePicker: (options) => { opened.theme = options; },
|
|
74
|
+
openAutoClearPicker: noop,
|
|
75
|
+
openProfilePicker: noop,
|
|
76
|
+
openMcpPicker: noop,
|
|
77
|
+
openPluginsPicker: noop,
|
|
78
|
+
openHooksPicker: noop,
|
|
79
|
+
openSkillsPicker: noop,
|
|
80
|
+
openMemoryCorePicker: noop,
|
|
81
|
+
openUpdatePicker: noop,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
test('Settings paints its loading destination before the daemon snapshot settles', async () => {
|
|
86
|
+
supersedePanelEpoch();
|
|
87
|
+
const host = createPanelHost();
|
|
88
|
+
host.setPicker({ title: 'Theme' });
|
|
89
|
+
const snapshotGate = deferred();
|
|
90
|
+
const opened = {};
|
|
91
|
+
const { openSettingsPicker } = createSettingsPicker(settingsDeps(
|
|
92
|
+
host,
|
|
93
|
+
{ getSettingsSnapshot: () => snapshotGate.promise },
|
|
94
|
+
opened,
|
|
95
|
+
));
|
|
96
|
+
|
|
97
|
+
const opening = openSettingsPicker();
|
|
98
|
+
assert.equal(host.current()?.title, 'Settings');
|
|
99
|
+
assert.match(host.current()?.description || '', /Loading settings/i);
|
|
100
|
+
assert.notEqual(host.current(), null);
|
|
101
|
+
|
|
102
|
+
snapshotGate.resolve({});
|
|
103
|
+
await opening;
|
|
104
|
+
const settings = host.current();
|
|
105
|
+
assert.equal(settings?.title, 'Settings');
|
|
106
|
+
assert.ok(settings.items.length > 0);
|
|
107
|
+
|
|
108
|
+
for (const action of ['model', 'workflow', 'output-style', 'theme']) {
|
|
109
|
+
const item = settings.items.find((entry) => entry._action === action);
|
|
110
|
+
settings.onSelect(item.value, item);
|
|
111
|
+
}
|
|
112
|
+
for (const options of [opened.model, opened.workflow, opened.outputStyle, opened.theme]) {
|
|
113
|
+
assert.equal(options?.handoffPanel?.title, 'Settings');
|
|
114
|
+
assert.equal(options?.handoffPanel?.pickerKey, 'settings-handoff');
|
|
115
|
+
assert.equal(options?.handoffPanel?.loading, true);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('Model save keeps a Settings handoff panel visible until the write settles', async () => {
|
|
120
|
+
supersedePanelEpoch();
|
|
121
|
+
const host = createPanelHost();
|
|
122
|
+
const saveGate = deferred();
|
|
123
|
+
const { openModelPicker } = createModelPicker({
|
|
124
|
+
store: {
|
|
125
|
+
pushNotice: () => {},
|
|
126
|
+
listProviderModels: async () => [],
|
|
127
|
+
setRoute: () => saveGate.promise,
|
|
128
|
+
},
|
|
129
|
+
getState: () => ({ provider: 'openai', model: 'gpt-5', effort: null, fast: false }),
|
|
130
|
+
surface: host.surface,
|
|
131
|
+
setProviderPrompt: () => {},
|
|
132
|
+
setSettingsPrompt: () => {},
|
|
133
|
+
providerModelsCacheRef: {
|
|
134
|
+
current: {
|
|
135
|
+
at: Date.now(),
|
|
136
|
+
models: [{ provider: 'openai', id: 'gpt-5', display: 'GPT-5' }],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
webSearchModelsCacheRef: { current: { models: [], at: 0 } },
|
|
140
|
+
modelPickerRequestRef: { current: 0 },
|
|
141
|
+
clearModelCaches: () => {},
|
|
142
|
+
modelSwitchNotice: () => 'switched',
|
|
143
|
+
openProviderSetupPicker: () => {},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await openModelPicker({
|
|
147
|
+
handoffPanel: HANDOFF,
|
|
148
|
+
onAfterSelect: () => host.setPicker({ title: 'Settings', items: [{ value: 'done' }] }),
|
|
149
|
+
});
|
|
150
|
+
const providers = host.current();
|
|
151
|
+
providers.onSelect(providers.items[0].value, providers.items[0]);
|
|
152
|
+
const models = host.current();
|
|
153
|
+
models.onSelect(models.items[0].value, models.items[0]);
|
|
154
|
+
|
|
155
|
+
assert.equal(host.current(), HANDOFF);
|
|
156
|
+
assert.equal(host.painted.at(-1), HANDOFF);
|
|
157
|
+
|
|
158
|
+
saveGate.resolve(true);
|
|
159
|
+
await flush();
|
|
160
|
+
assert.equal(host.current()?.title, 'Settings');
|
|
161
|
+
assert.notEqual(host.current(), HANDOFF);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
function routePicker(host, store) {
|
|
165
|
+
return createRoutePickers({
|
|
166
|
+
store: { pushNotice: () => {}, ...store },
|
|
167
|
+
state: {},
|
|
168
|
+
surface: host.surface,
|
|
169
|
+
setProviderPrompt: () => {},
|
|
170
|
+
setSettingsPrompt: () => {},
|
|
171
|
+
closeUsagePanel: () => {},
|
|
172
|
+
clean: (value) => value,
|
|
173
|
+
routeLabel: () => 'route',
|
|
174
|
+
agentModelParts: () => [],
|
|
175
|
+
agentModelProfile: () => 'profile',
|
|
176
|
+
workflowSwitchNotice: () => 'workflow switched',
|
|
177
|
+
openModelPicker: () => {},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
test('Workflow and output-style saves keep the Settings handoff visible until their writes settle', async () => {
|
|
182
|
+
supersedePanelEpoch();
|
|
183
|
+
const workflowHost = createPanelHost();
|
|
184
|
+
const workflowGate = deferred();
|
|
185
|
+
const workflow = routePicker(workflowHost, {
|
|
186
|
+
listWorkflows: async () => [{ id: 'solo', name: 'Solo', active: true }],
|
|
187
|
+
setWorkflow: () => workflowGate.promise,
|
|
188
|
+
});
|
|
189
|
+
await workflow.openWorkflowPicker({
|
|
190
|
+
returnTo: () => workflowHost.setPicker({ title: 'Settings', items: [{ value: 'done' }] }),
|
|
191
|
+
handoffPanel: HANDOFF,
|
|
192
|
+
});
|
|
193
|
+
const workflowPanel = workflowHost.current();
|
|
194
|
+
workflowPanel.onSelect(workflowPanel.items[0].value, workflowPanel.items[0]);
|
|
195
|
+
assert.equal(workflowHost.current(), HANDOFF);
|
|
196
|
+
|
|
197
|
+
workflowGate.resolve({ id: 'solo' });
|
|
198
|
+
await flush();
|
|
199
|
+
assert.equal(workflowHost.current()?.title, 'Settings');
|
|
200
|
+
|
|
201
|
+
const styleHost = createPanelHost();
|
|
202
|
+
const styleGate = deferred();
|
|
203
|
+
const outputStyle = routePicker(styleHost, {
|
|
204
|
+
listOutputStyles: async () => ({
|
|
205
|
+
current: { id: 'simple' },
|
|
206
|
+
styles: [{ id: 'simple', label: 'Simple' }],
|
|
207
|
+
}),
|
|
208
|
+
setOutputStyle: () => styleGate.promise,
|
|
209
|
+
});
|
|
210
|
+
await outputStyle.openOutputStylePicker({
|
|
211
|
+
returnTo: () => styleHost.setPicker({ title: 'Settings', items: [{ value: 'done' }] }),
|
|
212
|
+
handoffPanel: HANDOFF,
|
|
213
|
+
});
|
|
214
|
+
const stylePanel = styleHost.current();
|
|
215
|
+
stylePanel.onSelect(stylePanel.items[0].value, stylePanel.items[0]);
|
|
216
|
+
assert.equal(styleHost.current(), HANDOFF);
|
|
217
|
+
|
|
218
|
+
styleGate.resolve({ current: { id: 'simple', label: 'Simple' } });
|
|
219
|
+
await flush();
|
|
220
|
+
assert.equal(styleHost.current()?.title, 'Settings');
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('Theme selection hands directly to Settings without a null panel', () => {
|
|
224
|
+
supersedePanelEpoch();
|
|
225
|
+
const host = createPanelHost();
|
|
226
|
+
const { openThemePicker } = createThemeEffortPickers({
|
|
227
|
+
state: {},
|
|
228
|
+
store: {
|
|
229
|
+
listThemes: () => [{ id: 'basic', label: 'Basic' }],
|
|
230
|
+
getTheme: () => 'basic',
|
|
231
|
+
setTheme: (id) => ({ id, label: 'Basic' }),
|
|
232
|
+
pushNotice: () => {},
|
|
233
|
+
},
|
|
234
|
+
surface: host.surface,
|
|
235
|
+
setProviderPrompt: () => {},
|
|
236
|
+
setSettingsPrompt: () => {},
|
|
237
|
+
closeUsagePanel: () => {},
|
|
238
|
+
clean: (value) => value,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
openThemePicker({
|
|
242
|
+
returnTo: () => host.setPicker({ title: 'Settings', items: [{ value: 'done' }] }),
|
|
243
|
+
handoffPanel: HANDOFF,
|
|
244
|
+
});
|
|
245
|
+
const theme = host.current();
|
|
246
|
+
const paintedBefore = host.painted.length;
|
|
247
|
+
theme.onSelect(theme.items[0].value, theme.items[0]);
|
|
248
|
+
|
|
249
|
+
const transitionPaints = host.painted.slice(paintedBefore);
|
|
250
|
+
assert.deepEqual(transitionPaints.map((panel) => panel?.title), ['Settings', 'Settings']);
|
|
251
|
+
assert.ok(transitionPaints.every(Boolean));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
function workflowSlashDispatch(host, openWorkflowPicker, notices = []) {
|
|
255
|
+
return createSlashDispatch({
|
|
256
|
+
state: { busy: false, commandBusy: false },
|
|
257
|
+
store: { pushNotice: (message, tone) => notices.push([message, tone]) },
|
|
258
|
+
normalizeSlashCommandName: (value) => value,
|
|
259
|
+
surface: host.surface,
|
|
260
|
+
closeUsagePanel: () => {},
|
|
261
|
+
openWorkflowPicker,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
test('slash option entry keeps a loading panel until the async picker paints', async () => {
|
|
266
|
+
supersedePanelEpoch();
|
|
267
|
+
const host = createPanelHost();
|
|
268
|
+
const openGate = deferred();
|
|
269
|
+
const { runSlashCommand } = workflowSlashDispatch(host, () => {
|
|
270
|
+
const own = host.surface.claim();
|
|
271
|
+
return openGate.promise.then(() => own.paint({
|
|
272
|
+
title: 'Workflow',
|
|
273
|
+
description: 'Select active workflow.',
|
|
274
|
+
items: [{ value: 'solo', label: 'Solo' }],
|
|
275
|
+
}));
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
assert.equal(runSlashCommand('workflow'), true);
|
|
279
|
+
assert.equal(host.current()?._kind, 'slash-loading:workflow');
|
|
280
|
+
assert.equal(host.current()?.title, 'Workflow');
|
|
281
|
+
assert.equal(host.current()?.loading, true);
|
|
282
|
+
|
|
283
|
+
openGate.resolve();
|
|
284
|
+
await flush();
|
|
285
|
+
assert.equal(host.current()?.title, 'Workflow');
|
|
286
|
+
assert.equal(host.current()?._kind, undefined);
|
|
287
|
+
assert.ok(host.current()?.items.length > 0);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('Esc on slash loading prevents the pending picker from appearing later', async () => {
|
|
291
|
+
supersedePanelEpoch();
|
|
292
|
+
const host = createPanelHost();
|
|
293
|
+
const openGate = deferred();
|
|
294
|
+
const { runSlashCommand } = workflowSlashDispatch(host, () => {
|
|
295
|
+
const own = host.surface.claim();
|
|
296
|
+
return openGate.promise.then(() => own.paint({
|
|
297
|
+
title: 'Workflow',
|
|
298
|
+
items: [{ value: 'solo', label: 'Solo' }],
|
|
299
|
+
}));
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
runSlashCommand('workflow');
|
|
303
|
+
const loading = host.current();
|
|
304
|
+
assert.equal(loading?._kind, 'slash-loading:workflow');
|
|
305
|
+
loading.onCancel();
|
|
306
|
+
assert.equal(host.current(), null);
|
|
307
|
+
|
|
308
|
+
openGate.resolve();
|
|
309
|
+
await flush();
|
|
310
|
+
assert.equal(host.current(), null);
|
|
311
|
+
});
|
|
@@ -134,6 +134,7 @@ export function createProjectPicker({
|
|
|
134
134
|
description: 'Opening folder picker… choose a folder in the dialog window.',
|
|
135
135
|
help: 'Waiting for the system folder dialog…',
|
|
136
136
|
indexMode: 'never',
|
|
137
|
+
loading: true,
|
|
137
138
|
items: [],
|
|
138
139
|
onSelect: () => {},
|
|
139
140
|
onCancel: () => {},
|