mixdog 0.9.147 → 0.9.149
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/package.json +23 -3
- package/scripts/build-voice-runtime-unix.sh +101 -0
- package/scripts/embedding-quality-probe.mjs +96 -0
- package/scripts/generate-voice-runtime-manifest.mjs +300 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
- package/scripts/native-binary-arch.mjs +30 -13
- package/scripts/native-binary-arch.test.mjs +40 -4
- package/scripts/plan-voice-runtime-release.mjs +73 -0
- package/scripts/prepare-native-assets.mjs +50 -9
- package/scripts/prune-embedding-runtime.mjs +3 -3
- package/scripts/sync-voice-runtime-manifest.mjs +46 -0
- package/scripts/verify-release-assets.mjs +106 -6
- package/scripts/verify-voice-runtime-release.mjs +89 -0
- package/scripts/voice-runtime-config.json +52 -0
- package/src/app.mjs +42 -5
- package/src/headless-exec.mjs +58 -32
- package/src/headless-exec.test.mjs +74 -13
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/10-tool-workflow.md +7 -4
- package/src/rules/shared/30-exploration.md +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
- package/src/runtime/agent/orchestrator/config.mjs +53 -18
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/store.mjs +11 -3
- package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
- package/src/runtime/channels/lib/event-queue.mjs +51 -41
- package/src/runtime/channels/lib/scheduler.mjs +23 -28
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
- package/src/runtime/channels/lib/webhook.mjs +39 -20
- package/src/runtime/channels/lib/whisper-server.mjs +46 -1
- package/src/runtime/channels/lib/worker-main.mjs +3 -1
- package/src/runtime/media/adapters/gemini-video.mjs +20 -4
- package/src/runtime/media/jobs.mjs +13 -0
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/media/store.mjs +42 -22
- package/src/runtime/memory/index.mjs +4 -9
- package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
- package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
- package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
- package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
- package/src/runtime/memory/lib/ko-morph.mjs +46 -15
- package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
- package/src/runtime/memory/lib/memory.mjs +50 -12
- package/src/runtime/memory/lib/pg/process.mjs +1 -10
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
- package/src/runtime/memory/lib/query-handlers.mjs +2 -4
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
- package/src/runtime/shared/background-tasks.mjs +15 -0
- package/src/runtime/shared/background-tasks.test.mjs +78 -0
- package/src/runtime/shared/child-guardian.mjs +2 -10
- package/src/runtime/shared/pid-liveness.mjs +27 -0
- package/src/runtime/shared/pristine-execution-contract.json +0 -1
- package/src/runtime/shared/runtime-root.mjs +1 -11
- package/src/runtime/shared/service-discovery.mjs +1 -17
- package/src/runtime/shared/singleton-owner.mjs +1 -11
- package/src/runtime/shared/staged-update.mjs +2 -12
- package/src/runtime/shared/tool-status.mjs +5 -1
- package/src/runtime/shared/tool-surface.mjs +15 -7
- package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
- package/src/runtime/shared/turn-snapshot.mjs +137 -13
- package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
- package/src/runtime/shared/webhook-session-run.mjs +16 -2
- package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
- package/src/runtime/web-search/lib/state.mjs +15 -15
- package/src/runtime/web-search/lib/web-tools.mjs +46 -5
- package/src/session-runtime/context-status.mjs +4 -12
- package/src/session-runtime/runtime-core.mjs +20 -1
- package/src/session-runtime/session-lifecycle.mjs +9 -0
- package/src/standalone/agent-dispatch-broker.mjs +33 -2
- package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
- package/src/standalone/agent-tool/notify.mjs +3 -1
- package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
- package/src/standalone/agent-tool/tag-registry.mjs +116 -33
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/standalone/agent-tool/worker-index.mjs +8 -11
- package/src/standalone/agent-tool/worker-rows.mjs +4 -1
- package/src/standalone/agent-tool.mjs +20 -55
- package/src/standalone/agent-watchdog-registry.mjs +28 -4
- package/src/standalone/channel-client.mjs +47 -15
- package/src/standalone/channel-transport.mjs +44 -25
- package/src/standalone/daemon.mjs +31 -2
- package/src/standalone/hook-bus/config.mjs +25 -9
- package/src/standalone/hook-bus/handlers.mjs +16 -52
- package/src/standalone/hook-bus.mjs +29 -23
- package/src/standalone/memory-runtime-proxy.mjs +1 -16
- package/src/standalone/session-client.mjs +86 -23
- package/src/standalone/session-protocol.mjs +0 -2
- package/src/standalone/session-runtime-host-health.test.mjs +5 -4
- package/src/standalone/session-runtime-host.mjs +101 -6
- package/src/standalone/session-runtime-record.mjs +1 -5
- package/src/standalone/session-runtime-worker.mjs +52 -57
- package/src/standalone/session-service.mjs +4 -1
- package/src/standalone/session-transport.mjs +56 -25
- package/src/tui/App.jsx +40 -49
- package/src/tui/app/app-view.jsx +14 -21
- package/src/tui/app/core-memory-picker.mjs +29 -16
- package/src/tui/app/create-app-pickers.mjs +15 -36
- package/src/tui/app/extension-pickers.mjs +60 -49
- package/src/tui/app/maintenance-pickers.mjs +100 -57
- package/src/tui/app/message-selector.mjs +6 -5
- package/src/tui/app/model-picker.mjs +30 -13
- package/src/tui/app/onboarding-steps.mjs +21 -12
- package/src/tui/app/panel-epoch.mjs +56 -0
- package/src/tui/app/panel-epoch.test.mjs +1009 -0
- package/src/tui/app/panel-surface.mjs +98 -0
- package/src/tui/app/project-picker.mjs +30 -29
- package/src/tui/app/prompt-submit.mjs +192 -97
- package/src/tui/app/prompt-submit.test.mjs +206 -0
- package/src/tui/app/provider-setup-picker.mjs +74 -43
- package/src/tui/app/resume-picker.mjs +11 -8
- package/src/tui/app/route-pickers.mjs +45 -28
- package/src/tui/app/settings-picker.mjs +69 -18
- package/src/tui/app/shell-layout.mjs +1 -3
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +4 -2
- package/src/tui/app/text-entry-policy.mjs +25 -0
- package/src/tui/app/theme-effort-pickers.mjs +11 -16
- package/src/tui/app/usage-context-panels.mjs +21 -25
- package/src/tui/app/use-global-key-input.mjs +3 -2
- package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
- package/src/tui/app/use-prompt-handlers.mjs +3 -2
- package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
- package/src/tui/components/Picker.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +7 -2
- package/src/tui/dist/index.mjs +652 -561
- package/src/tui/lib/voice-setup.mjs +1 -2
- package/src/tui/session/agent-job-feed.mjs +73 -11
- package/src/tui/session/session-api-ext.mjs +5 -6
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +15 -5
- package/src/tui/session/tui-steering-persist.mjs +33 -19
- package/src/tui/session/tui-steering-persist.test.mjs +104 -0
- package/src/tui/session-local.mjs +0 -22
- package/scripts/.tmp-coverage-report.mjs +0 -96
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
|
@@ -5,32 +5,38 @@
|
|
|
5
5
|
// re-created per render so it closes over the CURRENT prompt/panel state.
|
|
6
6
|
import {
|
|
7
7
|
memoryCoreResultErrorText,
|
|
8
|
-
parseHookRuleInput,
|
|
9
8
|
parseMcpServerInput,
|
|
10
9
|
parseSkillInput,
|
|
11
10
|
} from './input-parsers.mjs';
|
|
12
11
|
import { projectNameFromPath } from './app-format.mjs';
|
|
12
|
+
import { isPanelEpochCurrent, supersedePanelEpoch } from './panel-epoch.mjs';
|
|
13
13
|
import {
|
|
14
14
|
buildPromptContentWithImages,
|
|
15
15
|
imageReferenceIds,
|
|
16
16
|
pastedTextReferenceIds,
|
|
17
17
|
} from '../paste-attachments.mjs';
|
|
18
18
|
|
|
19
|
+
// In-flight prompt writes, tracked at MODULE scope: createPromptSubmit is
|
|
20
|
+
// re-created on every render, so a closure flag resets mid-write and a second
|
|
21
|
+
// Enter starts an overlapping daemon write whose OLDER ack then closes the
|
|
22
|
+
// panel that already holds newer (restored) input. Each value is the panel
|
|
23
|
+
// epoch its write owns, so a write stops blocking the moment the user takes
|
|
24
|
+
// the surface back (Esc/close bumps the epoch).
|
|
25
|
+
let providerWriteToken = 0;
|
|
26
|
+
let settingsWriteToken = 0;
|
|
27
|
+
const providerWriteInFlight = () => providerWriteToken > 0 && isPanelEpochCurrent(providerWriteToken);
|
|
28
|
+
const settingsWriteInFlight = () => settingsWriteToken > 0 && isPanelEpochCurrent(settingsWriteToken);
|
|
29
|
+
|
|
19
30
|
export function createPromptSubmit({
|
|
20
31
|
store,
|
|
21
32
|
state,
|
|
22
33
|
providerPrompt,
|
|
23
|
-
channelPrompt,
|
|
24
|
-
hookPrompt,
|
|
25
34
|
settingsPrompt,
|
|
26
35
|
setProviderPrompt,
|
|
27
|
-
setChannelPrompt,
|
|
28
|
-
setHookPrompt,
|
|
29
36
|
setSettingsPrompt,
|
|
30
37
|
oauthSubmitRef,
|
|
31
38
|
clearModelCaches,
|
|
32
39
|
openProviderSetupPicker,
|
|
33
|
-
openHooksPicker,
|
|
34
40
|
openSettingsPicker,
|
|
35
41
|
openProjectPicker,
|
|
36
42
|
openAutoClearPicker,
|
|
@@ -54,6 +60,14 @@ export function createPromptSubmit({
|
|
|
54
60
|
}
|
|
55
61
|
return Promise.resolve(target.apply(store, args));
|
|
56
62
|
};
|
|
63
|
+
// Panel openers are async on a daemon-backed store: a bare call leaves a
|
|
64
|
+
// rejected open as an unhandled rejection, which terminates the TUI.
|
|
65
|
+
const openPanel = (open, ...args) => {
|
|
66
|
+
if (typeof open !== 'function') return;
|
|
67
|
+
void Promise.resolve(open(...args)).catch((error) => {
|
|
68
|
+
store.pushNotice(`panel failed to open: ${error?.message || error}`, 'error');
|
|
69
|
+
});
|
|
70
|
+
};
|
|
57
71
|
const submitPrompt = (prompt, options) => {
|
|
58
72
|
if (typeof store.submitAsync !== 'function') return store.submit(prompt, options);
|
|
59
73
|
void Promise.resolve(store.submitAsync(prompt, options)).then((accepted) => {
|
|
@@ -65,6 +79,17 @@ export function createPromptSubmit({
|
|
|
65
79
|
// durable intake, while provider execution continues independently.
|
|
66
80
|
return true;
|
|
67
81
|
};
|
|
82
|
+
// A rejected daemon write keeps the settings prompt OPEN with the typed
|
|
83
|
+
// value restored (the epoch remounts the editor, so an identical retry value
|
|
84
|
+
// still re-seeds it) instead of closing and losing the entry.
|
|
85
|
+
const restoreSettingsPrompt = (target, value) => {
|
|
86
|
+
setSettingsPrompt({
|
|
87
|
+
...target,
|
|
88
|
+
submitting: false,
|
|
89
|
+
initialValue: value,
|
|
90
|
+
restoreEpoch: (Number(target.restoreEpoch) || 0) + 1,
|
|
91
|
+
});
|
|
92
|
+
};
|
|
68
93
|
const onSubmit = (raw) => {
|
|
69
94
|
const text = String(raw ?? '');
|
|
70
95
|
const commandText = text.trim();
|
|
@@ -73,57 +98,94 @@ export function createPromptSubmit({
|
|
|
73
98
|
store.pushNotice('wait for the current command to finish', 'warn');
|
|
74
99
|
return false;
|
|
75
100
|
}
|
|
101
|
+
// Every provider write below is a DAEMON RPC (async). The prompt may only
|
|
102
|
+
// close once the write is acknowledged; a rejection keeps the panel open
|
|
103
|
+
// with the entered value restored so a transport hiccup never silently
|
|
104
|
+
// eats a credential or claims success.
|
|
105
|
+
const beginProviderSave = (target) => {
|
|
106
|
+
// A new submit supersedes every older in-flight write for this surface.
|
|
107
|
+
const token = supersedePanelEpoch();
|
|
108
|
+
providerWriteToken = token;
|
|
109
|
+
setProviderPrompt((prompt) => (prompt === target ? { ...prompt, submitting: true } : prompt));
|
|
110
|
+
return token;
|
|
111
|
+
};
|
|
112
|
+
const endProviderSave = (token) => {
|
|
113
|
+
if (providerWriteToken === token) providerWriteToken = 0;
|
|
114
|
+
};
|
|
115
|
+
const finishProviderSave = (target, token) => {
|
|
116
|
+
endProviderSave(token);
|
|
117
|
+
// Stale ack (newer submit, or the user closed the prompt): never close
|
|
118
|
+
// or navigate a surface this write no longer owns.
|
|
119
|
+
if (!isPanelEpochCurrent(token)) return;
|
|
120
|
+
setProviderPrompt(null);
|
|
121
|
+
if (target.afterSave) target.afterSave();
|
|
122
|
+
else openPanel(openProviderSetupPicker);
|
|
123
|
+
};
|
|
124
|
+
const failProviderSave = (target, value, message, token) => {
|
|
125
|
+
endProviderSave(token);
|
|
126
|
+
store.pushNotice(message, 'error');
|
|
127
|
+
if (!isPanelEpochCurrent(token)) return;
|
|
128
|
+
// The restored panel now owns the surface.
|
|
129
|
+
supersedePanelEpoch();
|
|
130
|
+
setProviderPrompt({
|
|
131
|
+
...target,
|
|
132
|
+
submitting: false,
|
|
133
|
+
initialValue: value,
|
|
134
|
+
restoreEpoch: (Number(target.restoreEpoch) || 0) + 1,
|
|
135
|
+
});
|
|
136
|
+
};
|
|
76
137
|
if (providerPrompt.kind === 'api-key') {
|
|
77
138
|
if (!commandText) {
|
|
78
139
|
store.pushNotice(`API key is required for ${providerPrompt.providerId}`, 'warn');
|
|
79
140
|
return false;
|
|
80
141
|
}
|
|
81
|
-
|
|
82
|
-
store.
|
|
83
|
-
clearModelCaches('all');
|
|
84
|
-
const afterSave = providerPrompt.afterSave;
|
|
85
|
-
setProviderPrompt(null);
|
|
86
|
-
if (afterSave) afterSave();
|
|
87
|
-
else void openProviderSetupPicker();
|
|
88
|
-
return true;
|
|
89
|
-
} catch (e) {
|
|
90
|
-
store.pushNotice(`api key save failed: ${e?.message || e}`, 'error');
|
|
142
|
+
if (providerWriteInFlight()) {
|
|
143
|
+
store.pushNotice('API key is already being saved', 'warn');
|
|
91
144
|
return false;
|
|
92
145
|
}
|
|
146
|
+
const target = providerPrompt;
|
|
147
|
+
const token = beginProviderSave(target);
|
|
148
|
+
void serviceCall('saveProviderApiKey', target.providerId, commandText)
|
|
149
|
+
.then(() => {
|
|
150
|
+
clearModelCaches('all');
|
|
151
|
+
finishProviderSave(target, token);
|
|
152
|
+
})
|
|
153
|
+
.catch((error) => failProviderSave(target, commandText, `api key save failed: ${error?.message || error}`, token));
|
|
154
|
+
return true;
|
|
93
155
|
}
|
|
94
156
|
if (providerPrompt.kind === 'openai-usage-session') {
|
|
95
157
|
if (!commandText) {
|
|
96
158
|
store.pushNotice('OpenAI usage session key is required for credit lookup', 'warn');
|
|
97
159
|
return false;
|
|
98
160
|
}
|
|
99
|
-
|
|
100
|
-
store.
|
|
101
|
-
const afterSave = providerPrompt.afterSave;
|
|
102
|
-
setProviderPrompt(null);
|
|
103
|
-
if (afterSave) afterSave();
|
|
104
|
-
else void openProviderSetupPicker();
|
|
105
|
-
return true;
|
|
106
|
-
} catch (e) {
|
|
107
|
-
store.pushNotice(`OpenAI usage auth save failed: ${e?.message || e}`, 'error');
|
|
161
|
+
if (providerWriteInFlight()) {
|
|
162
|
+
store.pushNotice('OpenAI usage session key is already being saved', 'warn');
|
|
108
163
|
return false;
|
|
109
164
|
}
|
|
165
|
+
const target = providerPrompt;
|
|
166
|
+
const token = beginProviderSave(target);
|
|
167
|
+
void serviceCall('saveOpenAIUsageSessionKey', commandText)
|
|
168
|
+
.then(() => finishProviderSave(target, token))
|
|
169
|
+
.catch((error) => failProviderSave(target, commandText, `OpenAI usage auth save failed: ${error?.message || error}`, token));
|
|
170
|
+
return true;
|
|
110
171
|
}
|
|
111
172
|
if (providerPrompt.kind === 'local-url') {
|
|
112
|
-
|
|
113
|
-
store.
|
|
114
|
-
enabled: true,
|
|
115
|
-
baseURL: commandText || providerPrompt.defaultURL,
|
|
116
|
-
});
|
|
117
|
-
clearModelCaches('all');
|
|
118
|
-
const afterSave = providerPrompt.afterSave;
|
|
119
|
-
setProviderPrompt(null);
|
|
120
|
-
if (afterSave) afterSave();
|
|
121
|
-
else void openProviderSetupPicker();
|
|
122
|
-
return true;
|
|
123
|
-
} catch (e) {
|
|
124
|
-
store.pushNotice(`local provider update failed: ${e?.message || e}`, 'error');
|
|
173
|
+
if (providerWriteInFlight()) {
|
|
174
|
+
store.pushNotice('local provider update is already running', 'warn');
|
|
125
175
|
return false;
|
|
126
176
|
}
|
|
177
|
+
const target = providerPrompt;
|
|
178
|
+
const token = beginProviderSave(target);
|
|
179
|
+
void serviceCall('setLocalProvider', target.providerId, {
|
|
180
|
+
enabled: true,
|
|
181
|
+
baseURL: commandText || target.defaultURL,
|
|
182
|
+
})
|
|
183
|
+
.then(() => {
|
|
184
|
+
clearModelCaches('all');
|
|
185
|
+
finishProviderSave(target, token);
|
|
186
|
+
})
|
|
187
|
+
.catch((error) => failProviderSave(target, commandText, `local provider update failed: ${error?.message || error}`, token));
|
|
188
|
+
return true;
|
|
127
189
|
}
|
|
128
190
|
if (providerPrompt.kind === 'oauth-code') {
|
|
129
191
|
if (!commandText) {
|
|
@@ -146,7 +208,7 @@ export function createPromptSubmit({
|
|
|
146
208
|
store.pushNotice(`${providerPrompt.providerName || 'OAuth'} login complete`, 'info');
|
|
147
209
|
if (successReturn) successReturn();
|
|
148
210
|
else if (afterSave) afterSave();
|
|
149
|
-
else
|
|
211
|
+
else openPanel(openProviderSetupPicker);
|
|
150
212
|
})
|
|
151
213
|
.catch((e) => {
|
|
152
214
|
oauthSubmitRef.current = false;
|
|
@@ -157,48 +219,61 @@ export function createPromptSubmit({
|
|
|
157
219
|
return true;
|
|
158
220
|
}
|
|
159
221
|
}
|
|
160
|
-
// Channel token/target
|
|
161
|
-
//
|
|
162
|
-
|
|
222
|
+
// Channel token/target and hook-rule text prompts are retired: channels
|
|
223
|
+
// moved to the PWA and hook rules are edited in the Hooks picker, so no
|
|
224
|
+
// opener sets those prompts any more.
|
|
225
|
+
if (settingsPrompt) {
|
|
163
226
|
if (state.commandBusy) {
|
|
164
227
|
store.pushNotice('wait for the current command to finish', 'warn');
|
|
165
228
|
return false;
|
|
166
229
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
store.addHookRule?.(parsed.rule);
|
|
175
|
-
setHookPrompt(null);
|
|
176
|
-
void openHooksPicker();
|
|
177
|
-
return true;
|
|
178
|
-
}
|
|
179
|
-
} catch (e) {
|
|
180
|
-
store.pushNotice(`hook update failed: ${e?.message || e}`, 'error');
|
|
181
|
-
return false;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
if (settingsPrompt) {
|
|
185
|
-
if (state.commandBusy) {
|
|
186
|
-
store.pushNotice('wait for the current command to finish', 'warn');
|
|
230
|
+
// Settings writes are daemon RPCs and the panel stays open until they
|
|
231
|
+
// ACK, so a second Enter used to start an OVERLAPPING write: the older
|
|
232
|
+
// ack then closed the prompt that already held the newer, restored value
|
|
233
|
+
// and the user's input was lost. One write at a time per surface, and a
|
|
234
|
+
// superseded ack is ignored below.
|
|
235
|
+
if (settingsWriteInFlight()) {
|
|
236
|
+
store.pushNotice('wait for the current settings change to finish', 'warn');
|
|
187
237
|
return false;
|
|
188
238
|
}
|
|
239
|
+
const beginSettingsSave = (target) => {
|
|
240
|
+
const token = supersedePanelEpoch();
|
|
241
|
+
settingsWriteToken = token;
|
|
242
|
+
setSettingsPrompt((prompt) => (prompt === target ? { ...prompt, submitting: true } : prompt));
|
|
243
|
+
return token;
|
|
244
|
+
};
|
|
245
|
+
const endSettingsSave = (token) => {
|
|
246
|
+
if (settingsWriteToken === token) settingsWriteToken = 0;
|
|
247
|
+
};
|
|
248
|
+
const finishSettingsSave = (token, after) => {
|
|
249
|
+
endSettingsSave(token);
|
|
250
|
+
// Stale ack: a newer submit or an Esc owns this surface now.
|
|
251
|
+
if (!isPanelEpochCurrent(token)) return;
|
|
252
|
+
setSettingsPrompt(null);
|
|
253
|
+
after?.();
|
|
254
|
+
};
|
|
255
|
+
const failSettingsSave = (target, value, message, token, tone = 'error') => {
|
|
256
|
+
endSettingsSave(token);
|
|
257
|
+
store.pushNotice(message, tone);
|
|
258
|
+
if (!isPanelEpochCurrent(token)) return;
|
|
259
|
+
// The restored panel now owns the surface.
|
|
260
|
+
supersedePanelEpoch();
|
|
261
|
+
restoreSettingsPrompt(target, value);
|
|
262
|
+
};
|
|
189
263
|
try {
|
|
190
264
|
if (settingsPrompt.kind === 'cwd') {
|
|
191
265
|
if (!commandText) {
|
|
192
266
|
store.pushNotice('working directory path is required', 'warn');
|
|
193
267
|
return false;
|
|
194
268
|
}
|
|
269
|
+
const target = settingsPrompt;
|
|
270
|
+
const token = beginSettingsSave(target);
|
|
195
271
|
void serviceCall('setCwd', commandText, {
|
|
196
272
|
message: `Project set: ${projectNameFromPath(commandText)}`,
|
|
197
273
|
}).then(() => {
|
|
198
|
-
|
|
199
|
-
void openSettingsPicker();
|
|
274
|
+
finishSettingsSave(token, () => openPanel(openSettingsPicker));
|
|
200
275
|
}).catch((error) => {
|
|
201
|
-
|
|
276
|
+
failSettingsSave(target, commandText, `project switch failed: ${error?.message || error}`, token);
|
|
202
277
|
});
|
|
203
278
|
return true;
|
|
204
279
|
}
|
|
@@ -207,17 +282,21 @@ export function createPromptSubmit({
|
|
|
207
282
|
store.pushNotice('project path is required', 'warn');
|
|
208
283
|
return false;
|
|
209
284
|
}
|
|
285
|
+
const target = settingsPrompt;
|
|
286
|
+
const token = beginSettingsSave(target);
|
|
210
287
|
void serviceCall('inspectProjectPath', commandText).then((result) => {
|
|
211
288
|
const path = String(result?.path || commandText);
|
|
212
289
|
if (result?.directory === true) {
|
|
213
|
-
|
|
214
|
-
void registerProject(path);
|
|
290
|
+
finishSettingsSave(token, () => openPanel(registerProject, path));
|
|
215
291
|
return;
|
|
216
292
|
}
|
|
217
293
|
if (result?.exists === true) {
|
|
218
|
-
|
|
294
|
+
failSettingsSave(target, commandText, `${path} is not a directory`, token, 'warn');
|
|
219
295
|
return;
|
|
220
296
|
}
|
|
297
|
+
endSettingsSave(token);
|
|
298
|
+
if (!isPanelEpochCurrent(token)) return;
|
|
299
|
+
supersedePanelEpoch();
|
|
221
300
|
setSettingsPrompt({
|
|
222
301
|
kind: 'project-create-confirm',
|
|
223
302
|
label: 'New project · Create folder?',
|
|
@@ -225,7 +304,7 @@ export function createPromptSubmit({
|
|
|
225
304
|
pendingPath: path,
|
|
226
305
|
});
|
|
227
306
|
}).catch((error) => {
|
|
228
|
-
|
|
307
|
+
failSettingsSave(target, commandText, `project path check failed: ${error?.message || error}`, token);
|
|
229
308
|
});
|
|
230
309
|
return true;
|
|
231
310
|
}
|
|
@@ -233,12 +312,16 @@ export function createPromptSubmit({
|
|
|
233
312
|
const pendingPath = String(settingsPrompt.pendingPath || '');
|
|
234
313
|
const answer = String(commandText || '').trim().toLowerCase();
|
|
235
314
|
if (answer === 'y' || answer === 'yes') {
|
|
315
|
+
const target = settingsPrompt;
|
|
316
|
+
const token = beginSettingsSave(target);
|
|
236
317
|
void serviceCall('ensureProjectDirectory', pendingPath).then((created) => {
|
|
237
|
-
|
|
238
|
-
void registerProject(created || pendingPath);
|
|
318
|
+
finishSettingsSave(token, () => openPanel(registerProject, created || pendingPath));
|
|
239
319
|
}).catch((error) => {
|
|
320
|
+
endSettingsSave(token);
|
|
240
321
|
store.pushNotice(`could not create folder: ${error?.message || error}`, 'error');
|
|
241
|
-
|
|
322
|
+
// The typed value here is only the y/n answer, so close rather
|
|
323
|
+
// than restore — but never close a surface we no longer own.
|
|
324
|
+
if (isPanelEpochCurrent(token)) setSettingsPrompt(null);
|
|
242
325
|
});
|
|
243
326
|
return true;
|
|
244
327
|
}
|
|
@@ -248,21 +331,29 @@ export function createPromptSubmit({
|
|
|
248
331
|
}
|
|
249
332
|
if (settingsPrompt.kind === 'project-rename') {
|
|
250
333
|
const targetPath = String(settingsPrompt.projectPath || '');
|
|
334
|
+
const target = settingsPrompt;
|
|
335
|
+
const token = beginSettingsSave(target);
|
|
251
336
|
void serviceCall('renameProject', targetPath, commandText).then((updated) => {
|
|
252
337
|
if (updated) {
|
|
253
338
|
store.pushNotice(`project renamed to "${updated.name}"`, 'info');
|
|
254
339
|
}
|
|
255
|
-
|
|
256
|
-
void openProjectPicker();
|
|
340
|
+
finishSettingsSave(token, () => openPanel(openProjectPicker));
|
|
257
341
|
}).catch((error) => {
|
|
258
|
-
|
|
342
|
+
failSettingsSave(target, commandText, `rename failed: ${error?.message || error}`, token);
|
|
259
343
|
});
|
|
260
344
|
return true;
|
|
261
345
|
}
|
|
262
346
|
if (settingsPrompt.kind === 'system-shell') {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
347
|
+
const target = settingsPrompt;
|
|
348
|
+
const token = beginSettingsSave(target);
|
|
349
|
+
// Empty is the documented reset to automatic selection.
|
|
350
|
+
void serviceCall('setSystemShell', commandText)
|
|
351
|
+
.then(() => {
|
|
352
|
+
finishSettingsSave(token, () => openPanel(openSettingsPicker));
|
|
353
|
+
})
|
|
354
|
+
.catch((error) => {
|
|
355
|
+
failSettingsSave(target, commandText, `system shell update failed: ${error?.message || error}`, token);
|
|
356
|
+
});
|
|
266
357
|
return true;
|
|
267
358
|
}
|
|
268
359
|
if (settingsPrompt.kind === 'autoclear-provider') {
|
|
@@ -271,28 +362,32 @@ export function createPromptSubmit({
|
|
|
271
362
|
store.pushNotice('auto-clear provider is missing', 'warn');
|
|
272
363
|
return false;
|
|
273
364
|
}
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
365
|
+
const duration = String(commandText || '').trim();
|
|
366
|
+
const target = settingsPrompt;
|
|
367
|
+
const token = beginSettingsSave(target);
|
|
368
|
+
// Empty is the documented reset to the built-in provider default.
|
|
369
|
+
void serviceCall('setAutoClear', duration ? { provider, duration } : { provider, resetProvider: true })
|
|
370
|
+
.then(() => {
|
|
371
|
+
store.pushNotice(duration ? `Auto-clear ${provider} default set to ${duration}` : `Auto-clear ${provider} default reset`, 'info');
|
|
372
|
+
finishSettingsSave(token, () => openPanel(openAutoClearPicker, { advanced: true, returnTo: target.returnTo }));
|
|
373
|
+
})
|
|
374
|
+
.catch((error) => {
|
|
375
|
+
failSettingsSave(target, duration, `autoclear failed: ${error?.message || error}`, token);
|
|
376
|
+
});
|
|
285
377
|
return true;
|
|
286
378
|
}
|
|
287
379
|
if (settingsPrompt.kind === 'profile-title') {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
380
|
+
const target = settingsPrompt;
|
|
381
|
+
const token = beginSettingsSave(target);
|
|
382
|
+
// Empty is the documented "clear the title" action.
|
|
383
|
+
void serviceCall('setProfile', { title: commandText })
|
|
384
|
+
.then(() => {
|
|
385
|
+
store.pushNotice(commandText ? `Title set to "${commandText.trim()}"` : 'Title cleared', 'info');
|
|
386
|
+
finishSettingsSave(token, () => openPanel(openProfilePicker));
|
|
387
|
+
})
|
|
388
|
+
.catch((error) => {
|
|
389
|
+
failSettingsSave(target, commandText, `profile update failed: ${error?.message || error}`, token);
|
|
390
|
+
});
|
|
296
391
|
return true;
|
|
297
392
|
}
|
|
298
393
|
if (settingsPrompt.kind === 'plugin-add') {
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Prompt-submit dispatcher contract for daemon-backed settings writes.
|
|
2
|
+
//
|
|
3
|
+
// Two properties are pinned here because both lose user input when they break:
|
|
4
|
+
// 1. one write per surface at a time (a fast second Enter must be refused,
|
|
5
|
+
// never started as an overlapping write), and
|
|
6
|
+
// 2. an ack that no longer owns the surface (superseded by Esc or by a newer
|
|
7
|
+
// submit) must not close the prompt, navigate, or claim success.
|
|
8
|
+
// Plus the documented clear-by-empty settings actions must reach the daemon.
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import test from 'node:test';
|
|
11
|
+
import { readFile } from 'node:fs/promises';
|
|
12
|
+
|
|
13
|
+
import { createPromptSubmit } from './prompt-submit.mjs';
|
|
14
|
+
import { supersedePanelEpoch } from './panel-epoch.mjs';
|
|
15
|
+
import { canSubmitTextEntry, textEntryClearsByEmpty } from './text-entry-policy.mjs';
|
|
16
|
+
|
|
17
|
+
function deferred() {
|
|
18
|
+
let resolve;
|
|
19
|
+
let reject;
|
|
20
|
+
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
|
|
21
|
+
return { promise, resolve, reject };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const flush = async () => {
|
|
25
|
+
for (let i = 0; i < 4; i += 1) await new Promise((resolve) => setImmediate(resolve));
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function createHarness({ settingsPrompt = null, store: storeOverrides = {} } = {}) {
|
|
29
|
+
const notices = [];
|
|
30
|
+
const settingsPromptSet = [];
|
|
31
|
+
const opened = [];
|
|
32
|
+
const store = {
|
|
33
|
+
pushNotice: (message, tone) => notices.push([message, tone]),
|
|
34
|
+
...storeOverrides,
|
|
35
|
+
};
|
|
36
|
+
const { onSubmit } = createPromptSubmit({
|
|
37
|
+
store,
|
|
38
|
+
state: { commandBusy: false },
|
|
39
|
+
providerPrompt: null,
|
|
40
|
+
settingsPrompt,
|
|
41
|
+
setProviderPrompt: () => {},
|
|
42
|
+
setSettingsPrompt: (next) => {
|
|
43
|
+
settingsPromptSet.push(typeof next === 'function' ? next(settingsPrompt) : next);
|
|
44
|
+
},
|
|
45
|
+
oauthSubmitRef: { current: false },
|
|
46
|
+
clearModelCaches: () => {},
|
|
47
|
+
openProviderSetupPicker: () => { opened.push('providers'); },
|
|
48
|
+
openSettingsPicker: () => { opened.push('settings'); },
|
|
49
|
+
openProjectPicker: () => { opened.push('projects'); },
|
|
50
|
+
openAutoClearPicker: () => { opened.push('autoclear'); },
|
|
51
|
+
openProfilePicker: () => { opened.push('profile'); },
|
|
52
|
+
openPluginsPicker: () => {},
|
|
53
|
+
openMcpServersPicker: () => {},
|
|
54
|
+
openProjectSkillsPicker: () => {},
|
|
55
|
+
openMemoryCorePicker: () => {},
|
|
56
|
+
registerProject: () => {},
|
|
57
|
+
runSlashCommand: () => true,
|
|
58
|
+
armTranscriptFollow: () => {},
|
|
59
|
+
clearPastedImagesSnapshot: () => {},
|
|
60
|
+
clearPastedTextsSnapshot: () => {},
|
|
61
|
+
pastedImagesRef: { current: {} },
|
|
62
|
+
pastedTextsRef: { current: {} },
|
|
63
|
+
});
|
|
64
|
+
return { onSubmit, notices, settingsPromptSet, opened };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const closedCount = (harness) => harness.settingsPromptSet.filter((value) => value === null).length;
|
|
68
|
+
|
|
69
|
+
test('a second settings submit is refused while the first daemon write is in flight', async () => {
|
|
70
|
+
supersedePanelEpoch();
|
|
71
|
+
const gate = deferred();
|
|
72
|
+
const shellCalls = [];
|
|
73
|
+
const harness = createHarness({
|
|
74
|
+
settingsPrompt: { kind: 'system-shell', label: 'System shell' },
|
|
75
|
+
store: {
|
|
76
|
+
setSystemShell: (command) => { shellCalls.push(command); return gate.promise; },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
assert.equal(harness.onSubmit('pwsh'), true);
|
|
81
|
+
// Fast second Enter (before React re-renders the prompt): refused, so the
|
|
82
|
+
// draft is kept and no overlapping write is started.
|
|
83
|
+
assert.equal(harness.onSubmit('bash'), false);
|
|
84
|
+
assert.deepEqual(shellCalls, ['pwsh']);
|
|
85
|
+
assert.ok(harness.notices.some(([message, tone]) => tone === 'warn' && /settings change/.test(message)));
|
|
86
|
+
|
|
87
|
+
gate.resolve(true);
|
|
88
|
+
await flush();
|
|
89
|
+
assert.equal(closedCount(harness), 1);
|
|
90
|
+
assert.deepEqual(harness.opened, ['settings']);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('an ack superseded by Esc and a newer submit never closes the live prompt', async () => {
|
|
94
|
+
supersedePanelEpoch();
|
|
95
|
+
const first = deferred();
|
|
96
|
+
const second = deferred();
|
|
97
|
+
|
|
98
|
+
const firstHarness = createHarness({
|
|
99
|
+
settingsPrompt: { kind: 'system-shell', label: 'System shell' },
|
|
100
|
+
store: { setSystemShell: () => first.promise },
|
|
101
|
+
});
|
|
102
|
+
assert.equal(firstHarness.onSubmit('pwsh'), true);
|
|
103
|
+
|
|
104
|
+
// Esc on the prompt (cancelSettingsPrompt) hands the surface back.
|
|
105
|
+
supersedePanelEpoch();
|
|
106
|
+
|
|
107
|
+
// A fresh prompt + submit: the abandoned write must not block it.
|
|
108
|
+
const secondHarness = createHarness({
|
|
109
|
+
settingsPrompt: { kind: 'system-shell', label: 'System shell' },
|
|
110
|
+
store: { setSystemShell: () => second.promise },
|
|
111
|
+
});
|
|
112
|
+
assert.equal(secondHarness.onSubmit('bash'), true);
|
|
113
|
+
|
|
114
|
+
first.resolve(true);
|
|
115
|
+
await flush();
|
|
116
|
+
// The older ack owns nothing: no close, no navigation, no success claim.
|
|
117
|
+
assert.equal(closedCount(firstHarness), 0);
|
|
118
|
+
assert.deepEqual(firstHarness.opened, []);
|
|
119
|
+
|
|
120
|
+
second.resolve(true);
|
|
121
|
+
await flush();
|
|
122
|
+
assert.equal(closedCount(secondHarness), 1);
|
|
123
|
+
assert.deepEqual(secondHarness.opened, ['settings']);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('a rejected settings write restores the typed value instead of closing', async () => {
|
|
127
|
+
supersedePanelEpoch();
|
|
128
|
+
const gate = deferred();
|
|
129
|
+
const harness = createHarness({
|
|
130
|
+
settingsPrompt: { kind: 'system-shell', label: 'System shell' },
|
|
131
|
+
store: { setSystemShell: () => gate.promise },
|
|
132
|
+
});
|
|
133
|
+
assert.equal(harness.onSubmit('pwsh -NoLogo'), true);
|
|
134
|
+
|
|
135
|
+
gate.reject(new Error('daemon offline'));
|
|
136
|
+
await flush();
|
|
137
|
+
|
|
138
|
+
assert.equal(closedCount(harness), 0);
|
|
139
|
+
const restored = harness.settingsPromptSet[harness.settingsPromptSet.length - 1];
|
|
140
|
+
assert.equal(restored.kind, 'system-shell');
|
|
141
|
+
assert.equal(restored.initialValue, 'pwsh -NoLogo');
|
|
142
|
+
assert.equal(restored.submitting, false);
|
|
143
|
+
assert.ok(restored.restoreEpoch >= 1);
|
|
144
|
+
assert.ok(harness.notices.some(([message, tone]) => tone === 'error' && /daemon offline/.test(message)));
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('empty submits reach the daemon as the documented reset/clear actions', async () => {
|
|
148
|
+
supersedePanelEpoch();
|
|
149
|
+
const shellCalls = [];
|
|
150
|
+
const shell = createHarness({
|
|
151
|
+
settingsPrompt: { kind: 'system-shell', label: 'System shell' },
|
|
152
|
+
store: { setSystemShell: (command) => { shellCalls.push(command); return Promise.resolve(true); } },
|
|
153
|
+
});
|
|
154
|
+
assert.equal(shell.onSubmit(''), true);
|
|
155
|
+
await flush();
|
|
156
|
+
assert.deepEqual(shellCalls, ['']);
|
|
157
|
+
assert.equal(closedCount(shell), 1);
|
|
158
|
+
|
|
159
|
+
supersedePanelEpoch();
|
|
160
|
+
const autoClearCalls = [];
|
|
161
|
+
const autoClear = createHarness({
|
|
162
|
+
settingsPrompt: { kind: 'autoclear-provider', label: 'Auto-clear · openai', provider: 'openai' },
|
|
163
|
+
store: { setAutoClear: (patch) => { autoClearCalls.push(patch); return Promise.resolve({ enabled: true }); } },
|
|
164
|
+
});
|
|
165
|
+
assert.equal(autoClear.onSubmit(''), true);
|
|
166
|
+
await flush();
|
|
167
|
+
assert.deepEqual(autoClearCalls, [{ provider: 'openai', resetProvider: true }]);
|
|
168
|
+
assert.ok(autoClear.notices.some(([message]) => /default reset/.test(message)));
|
|
169
|
+
|
|
170
|
+
supersedePanelEpoch();
|
|
171
|
+
const profileCalls = [];
|
|
172
|
+
const profile = createHarness({
|
|
173
|
+
settingsPrompt: { kind: 'profile-title', label: 'Profile · Title' },
|
|
174
|
+
store: { setProfile: (patch) => { profileCalls.push(patch); return Promise.resolve({ title: '' }); } },
|
|
175
|
+
});
|
|
176
|
+
assert.equal(profile.onSubmit(''), true);
|
|
177
|
+
await flush();
|
|
178
|
+
assert.deepEqual(profileCalls, [{ title: '' }]);
|
|
179
|
+
assert.ok(profile.notices.some(([message]) => /Title cleared/.test(message)));
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('the settings text-entry panel is wired to the clear-by-empty policy', async () => {
|
|
183
|
+
// The dispatcher cases above enter through onSubmit and therefore bypass
|
|
184
|
+
// TextEntryPanel entirely — they pass even when the view drops the prop and
|
|
185
|
+
// the panel refuses every blank submit. No JSX parser exists in this
|
|
186
|
+
// workspace, so the wiring is pinned as source text instead.
|
|
187
|
+
const view = await readFile(new URL('./app-view.jsx', import.meta.url), 'utf8');
|
|
188
|
+
assert.match(view, /import \{ textEntryClearsByEmpty \} from '\.\/text-entry-policy\.mjs';/);
|
|
189
|
+
assert.match(view, /allowEmpty=\{textEntryClearsByEmpty\(settingsPrompt\.kind\)\}/);
|
|
190
|
+
// The panel's own submit gate must consult the same policy value.
|
|
191
|
+
const panel = await readFile(new URL('../components/TextEntryPanel.jsx', import.meta.url), 'utf8');
|
|
192
|
+
assert.match(panel, /canSubmitTextEntry\(draftRef\.current\.value, allowEmpty\)/);
|
|
193
|
+
assert.match(panel, /canSubmitTextEntry\(next\.value, allowEmpty\)/);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('clear-by-empty prompt kinds bypass the blank-submit gate', () => {
|
|
197
|
+
assert.equal(textEntryClearsByEmpty('system-shell'), true);
|
|
198
|
+
assert.equal(textEntryClearsByEmpty('autoclear-provider'), true);
|
|
199
|
+
assert.equal(textEntryClearsByEmpty('profile-title'), true);
|
|
200
|
+
assert.equal(textEntryClearsByEmpty('core-add'), false);
|
|
201
|
+
assert.equal(textEntryClearsByEmpty(''), false);
|
|
202
|
+
assert.equal(canSubmitTextEntry('', true), true);
|
|
203
|
+
assert.equal(canSubmitTextEntry(' ', true), true);
|
|
204
|
+
assert.equal(canSubmitTextEntry('', false), false);
|
|
205
|
+
assert.equal(canSubmitTextEntry(' value ', false), true);
|
|
206
|
+
});
|