mixdog 0.9.95 → 0.9.96
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/LICENSES/Apache-2.0.txt +201 -0
- package/LICENSES/MIT.txt +56 -0
- package/LICENSES/codex-NOTICE.txt +6 -0
- package/NOTICE.md +46 -9
- package/package.json +12 -9
- package/scripts/.tmp-coverage-report.mjs +96 -0
- package/scripts/build-tui.mjs +17 -1
- package/scripts/fixtures/patch-replay-corpus.json +98 -0
- package/src/defaults/mixdog-config.template.json +5 -8
- package/src/rules/agent/30-explorer.md +7 -4
- package/src/rules/shared/01-tool.md +20 -29
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
- package/src/runtime/agent/orchestrator/config.mjs +246 -67
- package/src/runtime/agent/orchestrator/mcp/client.mjs +26 -11
- package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
- package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +28 -4
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -33
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -27
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -10
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
- package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -33
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
- package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +118 -18
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -23
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +23 -9
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +87 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +29 -1
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +10 -10
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +56 -8
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
- package/src/runtime/channels/lib/config.mjs +2 -2
- package/src/runtime/channels/lib/inbound-handler.mjs +1 -1
- package/src/runtime/channels/lib/scheduler.mjs +2 -2
- package/src/runtime/memory/index.mjs +5 -38
- package/src/runtime/memory/lib/agent-ipc.mjs +145 -81
- package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
- package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +12 -33
- package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
- package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
- package/src/runtime/memory/lib/http-router.mjs +8 -0
- package/src/runtime/memory/lib/ko-morph.mjs +3 -20
- package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
- package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
- package/src/runtime/memory/lib/pg/process.mjs +32 -9
- package/src/runtime/memory/tool-defs.mjs +1 -5
- package/src/runtime/search/tool-defs.mjs +2 -2
- package/src/runtime/shared/agent-route-config.mjs +119 -0
- package/src/runtime/shared/atomic-file.mjs +80 -4
- package/src/runtime/shared/channel-notification-routing.mjs +2 -2
- package/src/runtime/shared/child-guardian.mjs +204 -108
- package/src/runtime/shared/child-spawn-gate.mjs +60 -31
- package/src/runtime/shared/config.mjs +130 -44
- package/src/runtime/shared/resource-admission.mjs +146 -97
- package/src/runtime/shared/stream-progress.mjs +6 -0
- package/src/runtime/shared/tool-surface.mjs +0 -5
- package/src/runtime/shared/turn-snapshot.mjs +31 -5
- package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
- package/src/session-runtime/config-helpers.mjs +39 -37
- package/src/session-runtime/model-route-api.mjs +7 -5
- package/src/session-runtime/provider-auth-api.mjs +0 -7
- package/src/session-runtime/runtime-core.mjs +11 -11
- package/src/session-runtime/runtime-tunables.mjs +4 -4
- package/src/session-runtime/session-lifecycle.mjs +9 -2
- package/src/session-runtime/session-turn-api.mjs +41 -9
- package/src/session-runtime/settings-api.mjs +5 -17
- package/src/session-runtime/tool-defs.mjs +3 -3
- package/src/session-runtime/workflow-agents-api.mjs +55 -81
- package/src/session-runtime/workflow.mjs +14 -24
- package/src/standalone/agent-dispatch-broker.mjs +198 -0
- package/src/standalone/agent-tool/helpers.mjs +2 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
- package/src/standalone/agent-tool/tool-def.mjs +0 -14
- package/src/standalone/backend-daemon.mjs +97 -36
- package/src/standalone/channel-admin.mjs +12 -12
- package/src/standalone/channel-daemon-client.mjs +17 -1
- package/src/standalone/channel-daemon-transport.mjs +90 -8
- package/src/standalone/engine-daemon-client.mjs +380 -205
- package/src/standalone/engine-daemon-protocol.mjs +3 -4
- package/src/standalone/engine-daemon-service.mjs +378 -134
- package/src/standalone/engine-daemon-transport.mjs +268 -77
- package/src/standalone/explore-tool.mjs +187 -70
- package/src/standalone/fair-call-scheduler.mjs +264 -0
- package/src/standalone/session-protocol.mjs +188 -0
- package/src/tui/App.jsx +30 -17
- package/src/tui/app/app-view.jsx +5 -2
- package/src/tui/app/core-memory-picker.mjs +19 -19
- package/src/tui/app/doctor.mjs +5 -11
- package/src/tui/app/message-selector.mjs +103 -0
- package/src/tui/app/onboarding-steps.mjs +1 -2
- package/src/tui/app/project-picker.mjs +80 -56
- package/src/tui/app/prompt-submit.mjs +46 -41
- package/src/tui/app/resume-picker.mjs +2 -2
- package/src/tui/app/route-pickers.mjs +3 -1
- package/src/tui/app/settings-picker.mjs +6 -8
- package/src/tui/app/transcript-window.mjs +38 -0
- package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
- package/src/tui/app/use-prompt-handlers.mjs +86 -21
- package/src/tui/app/use-prompt-hint.mjs +3 -2
- package/src/tui/app/use-prompt-queue-history.mjs +10 -3
- package/src/tui/app/use-transcript-scroll.mjs +9 -2
- package/src/tui/app/use-transcript-window.mjs +50 -17
- package/src/tui/components/PromptInput.jsx +71 -38
- package/src/tui/components/Spinner.jsx +1 -1
- package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
- package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
- package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
- package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
- package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
- package/src/tui/dist/index.mjs +920 -1592
- package/src/tui/engine/live-share.mjs +34 -0
- package/src/tui/engine/session-api-ext.mjs +1 -4
- package/src/tui/engine/session-api.mjs +103 -33
- package/src/tui/engine/session-flow.mjs +7 -1
- package/src/tui/engine/tool-card-results.mjs +4 -2
- package/src/tui/engine/tool-result-text.mjs +10 -0
- package/src/tui/engine/turn.mjs +9 -69
- package/src/tui/engine-local-session.mjs +47 -31
- package/src/tui/index.jsx +1 -2
- package/src/tui/spinner-meta.mjs +4 -4
- package/src/tui/spinner-verbs.mjs +51 -3
- package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
|
@@ -709,3 +709,37 @@ export function createLiveShare({
|
|
|
709
709
|
},
|
|
710
710
|
};
|
|
711
711
|
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* ONE viewer-submit intake for EVERY engine entry point. The TUI store calls
|
|
715
|
+
* submit(); the daemon awaits submitAsync() (engine-daemon-service prefers it).
|
|
716
|
+
* Wrapping only one of them let the other path enqueue the prompt on the VIEWER
|
|
717
|
+
* engine, so that surface rendered its own local user row beside the owner's
|
|
718
|
+
* mirrored one — the same message twice on screen
|
|
719
|
+
* (user: 데스크탑에서만 입력이 두 개로 들어간다).
|
|
720
|
+
*
|
|
721
|
+
* `spool(submissionId)` is the durable fallback. The submission id is minted
|
|
722
|
+
* here when the caller has none, so the pipe frame and the spool entry carry
|
|
723
|
+
* the SAME identity and the owner reuses it for the settled user row — which is
|
|
724
|
+
* what releases the submitting surface's optimistic bubble.
|
|
725
|
+
*/
|
|
726
|
+
export function forwardViewerSubmit({ text, options = {}, share, spool, pid = process.pid }) {
|
|
727
|
+
const value = String(text || '').trim();
|
|
728
|
+
if (!value) return false;
|
|
729
|
+
// Reconcile first so a session that became attachable this event-loop turn
|
|
730
|
+
// takes the instant pipe path instead of the durable detour.
|
|
731
|
+
try { share.ensure?.(); } catch { /* durable fallback below */ }
|
|
732
|
+
const submissionId = options.id != null && String(options.id).trim()
|
|
733
|
+
? String(options.id).trim()
|
|
734
|
+
: `view-submit-${pid}-${Date.now()}`;
|
|
735
|
+
const deliverDurably = () => spool?.(submissionId) === true;
|
|
736
|
+
// A pipe WRITE is not a delivery: the owner may refuse the prompt and the
|
|
737
|
+
// socket may die between write and read, so an unacknowledged submit is
|
|
738
|
+
// re-delivered through the spool — late, never lost.
|
|
739
|
+
if (share.sendSubmit(value, {
|
|
740
|
+
id: submissionId,
|
|
741
|
+
submittedAt: options.submittedAt,
|
|
742
|
+
onUndelivered: deliverDurably,
|
|
743
|
+
})) return true;
|
|
744
|
+
return deliverDurably();
|
|
745
|
+
}
|
|
@@ -623,9 +623,6 @@ export function createEngineApiB(bag) {
|
|
|
623
623
|
setAgentRoute: async (agentId, opts) => {
|
|
624
624
|
return await runtime.setAgentRoute?.(agentId, opts);
|
|
625
625
|
},
|
|
626
|
-
setDefaultProvider: async (provider) => {
|
|
627
|
-
return await runtime.setDefaultProvider?.(provider);
|
|
628
|
-
},
|
|
629
626
|
listProviders: () => {
|
|
630
627
|
return runtime.listProviders();
|
|
631
628
|
},
|
|
@@ -883,7 +880,7 @@ export function createEngineApiB(bag) {
|
|
|
883
880
|
renameSessionTitle: (id, title) => {
|
|
884
881
|
return runtime.renameSessionTitle?.(id, title) ?? false;
|
|
885
882
|
},
|
|
886
|
-
// Desktop sidebar watcher hook:
|
|
883
|
+
// Desktop sidebar watcher hook: the backend watches this directory so
|
|
887
884
|
// heartbeat sidecar create/delete pushes instant working/dot updates.
|
|
888
885
|
// Without it the watcher silently no-ops and the sidebar falls back to
|
|
889
886
|
// the 60s safety poll (user: spinner kept spinning after the turn ended).
|
|
@@ -7,8 +7,7 @@ import { isQueuedEntryEditable, promptDisplayText } from './queue-helpers.mjs';
|
|
|
7
7
|
import { createEngineApiB } from './session-api-ext.mjs';
|
|
8
8
|
import { buildDoctorReport } from '../app/doctor.mjs';
|
|
9
9
|
import { recomputePromptHistory } from './prompt-history.mjs';
|
|
10
|
-
import { buildMergedPromptHistory, loadPromptHistory } from '../prompt-history-store.mjs';
|
|
11
|
-
import { touchProjectSelected } from '../../standalone/projects.mjs';
|
|
10
|
+
import { appendPromptHistory, buildMergedPromptHistory, loadPromptHistory, promptHistoryKey } from '../prompt-history-store.mjs';
|
|
12
11
|
|
|
13
12
|
// Upper bound on how long a manual Esc abort may leave the store busy while the
|
|
14
13
|
// in-flight runtime.ask() unwinds. runtime.abort() normally rejects the pending
|
|
@@ -33,7 +32,7 @@ export function createEngineApi(bag) {
|
|
|
33
32
|
const snapshot = {
|
|
34
33
|
autoClear: await read(api.getAutoClear),
|
|
35
34
|
compaction: await read(api.getCompactionSettings),
|
|
36
|
-
|
|
35
|
+
recap: await read(api.getRecapSettings),
|
|
37
36
|
channels: await read(api.getChannelSettings, { includeStatus: false }),
|
|
38
37
|
systemShell: await read(api.getSystemShell),
|
|
39
38
|
outputStyle: (await read(api.getOutputStyle)) || (await read(api.listOutputStyles)),
|
|
@@ -58,27 +57,34 @@ export function createEngineApiA(bag) {
|
|
|
58
57
|
const {
|
|
59
58
|
runtime, nextId, flags, pending, listeners, getState, getPublishedState = getState, set, flushEmitImmediate, pushItem, patchItem, replaceItems, restoreOlderTranscript, restoreNewerTranscript, settleStreamingTail, clearStreamingTail, pushNotice, autoClearState, agentStatusState, routeState, syncContextStats, denyAllToolApprovals, updateAgentJobCard, requeueEntriesFront, enqueue, autoClearBeforeSubmit, restoreQueued, resetStatsAndSyncContext, drain, flushDeferredExecutionPendingResumeKick, discardExecutionPendingResume,
|
|
60
59
|
} = bag;
|
|
61
|
-
const
|
|
60
|
+
const submission = (text, options = {}) => {
|
|
62
61
|
const t = promptDisplayText(text, options).trim();
|
|
63
|
-
if (!t) return
|
|
62
|
+
if (!t) return null;
|
|
64
63
|
const mode = options.mode || 'prompt';
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
// Prompt input queued while a turn is active keeps the default `next`
|
|
65
|
+
// priority, so it is injected at the next tool/model boundary. Explicit
|
|
66
|
+
// options.priority still wins.
|
|
68
67
|
const priority = options.priority;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
68
|
+
return {
|
|
69
|
+
text,
|
|
70
|
+
queueOptions: {
|
|
71
|
+
...options,
|
|
72
|
+
// Always mint a submission id so daemon retries / dual-path intake
|
|
73
|
+
// can dedupe instead of booking the same prompt twice.
|
|
74
|
+
id: String(options.id || '').trim() || nextId(),
|
|
75
|
+
mode,
|
|
76
|
+
displayText: promptDisplayText(text, options),
|
|
77
|
+
priority,
|
|
78
|
+
},
|
|
77
79
|
};
|
|
80
|
+
};
|
|
81
|
+
const submit = (text, options = {}) => {
|
|
82
|
+
const intake = submission(text, options);
|
|
83
|
+
if (!intake) return false;
|
|
78
84
|
// A running clear (idle auto-clear or session_manage) sets commandBusy;
|
|
79
85
|
// queue the prompt instead of dropping it — it drains after the clear.
|
|
80
86
|
if (flags.autoClearRunning) {
|
|
81
|
-
return enqueue(text, queueOptions) !== false;
|
|
87
|
+
return enqueue(intake.text, intake.queueOptions) !== false;
|
|
82
88
|
}
|
|
83
89
|
// Any in-flight session command (clear/setModel/newSession/resume/...)
|
|
84
90
|
// holds commandBusy. Previously the prompt was dropped here and only the
|
|
@@ -86,17 +92,31 @@ export function createEngineApiA(bag) {
|
|
|
86
92
|
// commandBusy, and the central release hook re-kicks drain once the
|
|
87
93
|
// command settles, so the prompt runs afterwards rather than vanishing.
|
|
88
94
|
if (getState().commandBusy) {
|
|
89
|
-
return enqueue(text, queueOptions) !== false;
|
|
95
|
+
return enqueue(intake.text, intake.queueOptions) !== false;
|
|
90
96
|
}
|
|
91
97
|
if (getState().busy) {
|
|
92
|
-
return enqueue(text, queueOptions) !== false;
|
|
98
|
+
return enqueue(intake.text, intake.queueOptions) !== false;
|
|
93
99
|
}
|
|
94
100
|
// If autoClearBeforeSubmit rejects (e.g. compaction timeout throws), the
|
|
95
101
|
// prompt must still be queued — swallow the rejection so enqueue always
|
|
96
102
|
// runs and the submit is never silently lost.
|
|
97
|
-
void autoClearBeforeSubmit().catch(() => {}).then(
|
|
103
|
+
void autoClearBeforeSubmit().catch(() => {}).then(
|
|
104
|
+
() => enqueue(intake.text, intake.queueOptions),
|
|
105
|
+
);
|
|
98
106
|
return true;
|
|
99
107
|
};
|
|
108
|
+
const submitAsync = async (text, options = {}) => {
|
|
109
|
+
const intake = submission(text, options);
|
|
110
|
+
if (!intake) return false;
|
|
111
|
+
// Daemon intake needs an acknowledgement boundary stronger than the TUI's
|
|
112
|
+
// synchronous submit(): by the time this Promise resolves, the prompt is
|
|
113
|
+
// present either as a visible queue entry or as the first durable user row.
|
|
114
|
+
// Provider execution still runs detached through drain().
|
|
115
|
+
if (!flags.autoClearRunning && !getState().commandBusy && !getState().busy) {
|
|
116
|
+
await autoClearBeforeSubmit().catch(() => {});
|
|
117
|
+
}
|
|
118
|
+
return enqueue(intake.text, intake.queueOptions) !== false;
|
|
119
|
+
};
|
|
100
120
|
return {
|
|
101
121
|
getState: () => getPublishedState(),
|
|
102
122
|
patchItem,
|
|
@@ -107,7 +127,7 @@ export function createEngineApiA(bag) {
|
|
|
107
127
|
return () => listeners.delete(listener);
|
|
108
128
|
},
|
|
109
129
|
submit,
|
|
110
|
-
submitAsync
|
|
130
|
+
submitAsync,
|
|
111
131
|
reserveSession: (sessionId) => {
|
|
112
132
|
const id = runtime.reserveSessionId?.(sessionId);
|
|
113
133
|
if (!id) return false;
|
|
@@ -116,6 +136,46 @@ export function createEngineApiA(bag) {
|
|
|
116
136
|
return String(getState().sessionId || '') === String(id);
|
|
117
137
|
},
|
|
118
138
|
restoreQueued,
|
|
139
|
+
// Claude Code's message selector ("jump back to a previous message"):
|
|
140
|
+
// rewind the conversation to just before a user prompt and hand its text
|
|
141
|
+
// back for editing. Idle-only — a live turn must be interrupted first.
|
|
142
|
+
rewindToItem: async (itemId) => {
|
|
143
|
+
const target = String(itemId ?? '').trim();
|
|
144
|
+
if (!target) return null;
|
|
145
|
+
if (getState().busy || getState().commandBusy) return null;
|
|
146
|
+
const items = getState().items || [];
|
|
147
|
+
const index = items.findIndex(
|
|
148
|
+
(item) => item?.kind === 'user' && String(item?.id) === target,
|
|
149
|
+
);
|
|
150
|
+
if (index < 0) return null;
|
|
151
|
+
const text = String(items[index]?.text || '').trim();
|
|
152
|
+
if (!text) return null;
|
|
153
|
+
set({ commandBusy: true });
|
|
154
|
+
try {
|
|
155
|
+
const rewound = await runtime.rewindMessages?.({ text });
|
|
156
|
+
if (!rewound) return null;
|
|
157
|
+
const restoreKey = promptHistoryKey(text);
|
|
158
|
+
set({
|
|
159
|
+
items: replaceItems(items.slice(0, index), {
|
|
160
|
+
preserveSpill: true,
|
|
161
|
+
preserveTranscriptView: true,
|
|
162
|
+
}),
|
|
163
|
+
spinner: null,
|
|
164
|
+
thinking: null,
|
|
165
|
+
lastTurn: null,
|
|
166
|
+
// The prompt returns to the draft, so it must not ALSO sit in the
|
|
167
|
+
// Up-arrow history — otherwise it shows up twice.
|
|
168
|
+
promptHistoryList: (getState().promptHistoryList || [])
|
|
169
|
+
.filter((entry) => promptHistoryKey(entry) !== restoreKey),
|
|
170
|
+
});
|
|
171
|
+
syncContextStats({ allowEstimated: true });
|
|
172
|
+
set({ stats: { ...getState().stats } });
|
|
173
|
+
flushEmitImmediate?.();
|
|
174
|
+
return { text, removed: items.length - index, messages: rewound.removed };
|
|
175
|
+
} finally {
|
|
176
|
+
set({ commandBusy: false });
|
|
177
|
+
}
|
|
178
|
+
},
|
|
119
179
|
setModel: async (m) => {
|
|
120
180
|
if (getState().commandBusy) return false;
|
|
121
181
|
set({ commandBusy: true });
|
|
@@ -209,14 +269,14 @@ export function createEngineApiA(bag) {
|
|
|
209
269
|
set({ commandBusy: false });
|
|
210
270
|
}
|
|
211
271
|
},
|
|
212
|
-
|
|
213
|
-
return runtime.
|
|
272
|
+
getRecapSettings: () => {
|
|
273
|
+
return runtime.getRecapSettings?.() || { enabled: true };
|
|
214
274
|
},
|
|
215
|
-
|
|
275
|
+
setRecapEnabled: async (enabled) => {
|
|
216
276
|
if (getState().commandBusy) return null;
|
|
217
277
|
set({ commandBusy: true });
|
|
218
278
|
try {
|
|
219
|
-
const next = await runtime.
|
|
279
|
+
const next = await runtime.setRecapEnabled?.(enabled);
|
|
220
280
|
set({ ...routeState(), stats: { ...getState().stats } });
|
|
221
281
|
// Deferred for the same reason as setCompactionSettings above: keep
|
|
222
282
|
// the recompute off the key-handler tick so the toggle repaints
|
|
@@ -296,12 +356,6 @@ export function createEngineApiA(bag) {
|
|
|
296
356
|
},
|
|
297
357
|
setCwd: (path, options = {}) => {
|
|
298
358
|
const next = runtime.setCwd(path);
|
|
299
|
-
// "Last opened" recency belongs to the ACTUAL cwd switch, not to one
|
|
300
|
-
// caller: the project picker, its Current Path row and the /cwd prompt
|
|
301
|
-
// all land here, so the project list stays ordered by what was really
|
|
302
|
-
// opened last. Unregistered folders are ignored — this never registers
|
|
303
|
-
// a project on its own (addProject remains the only registration path).
|
|
304
|
-
try { touchProjectSelected(next); } catch { /* best-effort recency */ }
|
|
305
359
|
// Republish up-arrow history for the NEW project: current session prompts
|
|
306
360
|
// merged with the cwd-scoped persisted store for the new cwd.
|
|
307
361
|
const sessionList = recomputePromptHistory(getState().items);
|
|
@@ -311,6 +365,15 @@ export function createEngineApiA(bag) {
|
|
|
311
365
|
}
|
|
312
366
|
return next;
|
|
313
367
|
},
|
|
368
|
+
rememberPromptHistory: (value) => {
|
|
369
|
+
const text = String(value || '').trim();
|
|
370
|
+
if (!text) return false;
|
|
371
|
+
const persisted = appendPromptHistory(getState().cwd, text);
|
|
372
|
+
if (!persisted) return false;
|
|
373
|
+
const sessionList = recomputePromptHistory(getState().items);
|
|
374
|
+
set({ promptHistoryList: buildMergedPromptHistory(sessionList, persisted) });
|
|
375
|
+
return true;
|
|
376
|
+
},
|
|
314
377
|
getSystemShell: () => {
|
|
315
378
|
return runtime.getSystemShell?.() || runtime.systemShell || { source: 'auto', command: '', effective: '' };
|
|
316
379
|
},
|
|
@@ -608,7 +671,7 @@ export function createEngineApiA(bag) {
|
|
|
608
671
|
set({ commandBusy: false, commandStatus: null });
|
|
609
672
|
}
|
|
610
673
|
},
|
|
611
|
-
abort: () => {
|
|
674
|
+
abort: (options = {}) => {
|
|
612
675
|
if (!getState().busy) return false;
|
|
613
676
|
denyAllToolApprovals('interrupted by user');
|
|
614
677
|
const restoreState = flags.activePromptRestore;
|
|
@@ -616,7 +679,9 @@ export function createEngineApiA(bag) {
|
|
|
616
679
|
// interrupting should just cancel the running turn and let the steering
|
|
617
680
|
// prompt run next, NOT resurrect the in-flight prompt back into the draft.
|
|
618
681
|
const hasPendingSteering = pending.some((entry) => isQueuedEntryEditable(entry));
|
|
619
|
-
const canRestore =
|
|
682
|
+
const canRestore = options?.restorePrompt !== false
|
|
683
|
+
&& restoreState?.restorable
|
|
684
|
+
&& !hasPendingSteering;
|
|
620
685
|
const restoreText = canRestore ? restoreState.text : '';
|
|
621
686
|
const restorePastedImages = canRestore && restoreState?.pastedImages ? restoreState.pastedImages : null;
|
|
622
687
|
const restorePastedTexts = canRestore && restoreState?.pastedTexts ? restoreState.pastedTexts : null;
|
|
@@ -644,6 +709,11 @@ export function createEngineApiA(bag) {
|
|
|
644
709
|
restoreState.reclaimed = true;
|
|
645
710
|
const idSet = new Set((restoreState.submittedIds || []).filter((id) => id != null));
|
|
646
711
|
const patch = { spinner: null, thinking: null, lastTurn: null };
|
|
712
|
+
if (restoreText) {
|
|
713
|
+
const restoreKey = promptHistoryKey(restoreText);
|
|
714
|
+
patch.promptHistoryList = (getState().promptHistoryList || [])
|
|
715
|
+
.filter((entry) => promptHistoryKey(entry) !== restoreKey);
|
|
716
|
+
}
|
|
647
717
|
if (idSet.size > 0) {
|
|
648
718
|
const items = getState().items.filter((item) => !idSet.has(item?.id));
|
|
649
719
|
if (items.length !== getState().items.length) {
|
|
@@ -560,7 +560,13 @@ export function createSessionFlow(bag) {
|
|
|
560
560
|
removeQueuedEntries(queued);
|
|
561
561
|
const queuedText = queued.map((item) => item.text).filter((text) => String(text || '').trim()).join('\n');
|
|
562
562
|
const combinedText = [queuedText, String(currentText || '')].filter((text) => text.trim()).join('\n');
|
|
563
|
-
return {
|
|
563
|
+
return {
|
|
564
|
+
count: queued.length,
|
|
565
|
+
ids: queued.map((item) => String(item.id || '')).filter(Boolean),
|
|
566
|
+
text: combinedText,
|
|
567
|
+
pastedImages: mergePastedImages(queued),
|
|
568
|
+
pastedTexts: mergePastedTexts(queued),
|
|
569
|
+
};
|
|
564
570
|
}
|
|
565
571
|
|
|
566
572
|
const resetStats = () => {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* is the original engine.mjs logic verbatim.
|
|
12
12
|
*/
|
|
13
13
|
import { summarizeToolResult, aggregateDoneCategories } from '../../runtime/shared/tool-surface.mjs';
|
|
14
|
-
import { toolResultText, toolErrorDisplay, toolGroupedDisplayFallback } from './tool-result-text.mjs';
|
|
14
|
+
import { toolResultText, toolErrorDisplay, toolGroupedDisplayFallback, stripShellExitHeader } from './tool-result-text.mjs';
|
|
15
15
|
import { toolResultCallId } from './tool-call-fields.mjs';
|
|
16
16
|
import { parseAgentJob } from './agent-envelope.mjs';
|
|
17
17
|
import {
|
|
@@ -82,7 +82,9 @@ export function createToolCardResults({
|
|
|
82
82
|
// outcomes remain successful calls with their raw/semantic result detail.
|
|
83
83
|
const { exitCode, isExitError, isCallError } = toolCallOutcome(message, rawText);
|
|
84
84
|
const isError = isCallError;
|
|
85
|
-
const text = isError
|
|
85
|
+
const text = isError
|
|
86
|
+
? toolErrorDisplay(rawText, card?.name || 'tool')
|
|
87
|
+
: (isExitError ? stripShellExitHeader(rawText) : rawText);
|
|
86
88
|
|
|
87
89
|
if (aggregate && card.itemId === aggregate.itemId) {
|
|
88
90
|
if (!callRec) return false;
|
|
@@ -124,3 +124,13 @@ export function toolErrorDisplay(value, surface = 'tool') {
|
|
|
124
124
|
}
|
|
125
125
|
return /^error\s*:/i.test(text) ? text : `Error: ${text}`;
|
|
126
126
|
}
|
|
127
|
+
|
|
128
|
+
// Display-only: a command-exit card already renders `Exit N` as its state, so
|
|
129
|
+
// the machine header (`Error: [shell-run-failed] [exit code: N]`) is redundant
|
|
130
|
+
// in the body — and the bare word "Error" reads as a tool failure even though
|
|
131
|
+
// the tool ran fine and the command merely exited non-zero. The model-facing
|
|
132
|
+
// result keeps the header untouched; only the rendered body drops it.
|
|
133
|
+
export function stripShellExitHeader(text) {
|
|
134
|
+
const body = String(text ?? '');
|
|
135
|
+
return body.replace(/(^|\n)(?:Error:\s*)?\[shell-run-failed\][^\n]*\n{0,2}/i, '$1');
|
|
136
|
+
}
|
package/src/tui/engine/turn.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { aggregateToolCategoryEntry, aggregateDoneCategories, classifyToolCategory, formatAggregateDetail, summarizeToolResult } from '../../runtime/shared/tool-surface.mjs';
|
|
5
5
|
import { applyUsageDelta } from './session-stats.mjs';
|
|
6
6
|
import { pickVerb, pickDoneVerb, compactEventLabel, compactEventDetail } from './labels.mjs';
|
|
7
|
-
import { toolResultText, toolErrorDisplay } from './tool-result-text.mjs';
|
|
7
|
+
import { toolResultText, toolErrorDisplay, stripShellExitHeader } from './tool-result-text.mjs';
|
|
8
8
|
import { toolCallId, toolResultCallId, toolCallName, toolCallArgs } from './tool-call-fields.mjs';
|
|
9
9
|
import { promptDisplayText, STEERING_SUPPRESSED_DISPLAY } from './queue-helpers.mjs';
|
|
10
10
|
import { TUI_FRAME_MS, yieldToRenderer } from './render-timing.mjs';
|
|
@@ -183,12 +183,6 @@ export function createRunTurn(bag) {
|
|
|
183
183
|
const toolCards = [];
|
|
184
184
|
const toolGroups = new Map();
|
|
185
185
|
const resultsDone = new Set();
|
|
186
|
-
// apply_patch+post_shell twin display: parent callId → synthetic shell
|
|
187
|
-
// callId. The protocol stays ONE call/ONE result, but the transcript shows
|
|
188
|
-
// TWO cards (edit + verify shell) so a long post_shell no longer looks
|
|
189
|
-
// like a stuck 'Editing 1 file'. deliverToolResultMessage splits the
|
|
190
|
-
// combined result text back between the two cards.
|
|
191
|
-
const postShellTwins = new Map();
|
|
192
186
|
// ── Live shell-output tail → running tool card ─────────────────────────
|
|
193
187
|
// 1 s poll of orchestrator liveness while this turn runs. When exactly one
|
|
194
188
|
// standalone (non-aggregate) card is unresolved and the runtime reports a
|
|
@@ -748,7 +742,9 @@ export function createRunTurn(bag) {
|
|
|
748
742
|
// the provider's isError/error-tool envelope drives failure counts/red.
|
|
749
743
|
const { exitCode, isExitError, isCallError } = toolCallOutcome(message, rawText);
|
|
750
744
|
const isError = isCallError;
|
|
751
|
-
const text = isError
|
|
745
|
+
const text = isError
|
|
746
|
+
? toolErrorDisplay(rawText, callRec.name || 'tool')
|
|
747
|
+
: (isExitError ? stripShellExitHeader(rawText) : rawText);
|
|
752
748
|
callRec.summary = !isError ? summarizeToolResult(callRec.name, callRec.args, rawText, isError) : null;
|
|
753
749
|
assignAggregateSummaryOrder(aggregate, callRec);
|
|
754
750
|
callRec.isError = isError;
|
|
@@ -805,51 +801,9 @@ export function createRunTurn(bag) {
|
|
|
805
801
|
const earlyCallId = toolResultCallId(message);
|
|
806
802
|
if (earlyCallId) {
|
|
807
803
|
markToolCardCompletedState(earlyCallId, message);
|
|
808
|
-
const earlyTwin = postShellTwins.get(earlyCallId);
|
|
809
|
-
if (earlyTwin) markToolCardCompletedState(earlyTwin, message);
|
|
810
804
|
}
|
|
811
805
|
return;
|
|
812
806
|
}
|
|
813
|
-
const parentCallId = toolResultCallId(message);
|
|
814
|
-
const twinCallId = parentCallId ? postShellTwins.get(parentCallId) : null;
|
|
815
|
-
if (twinCallId && !resultsDone.has(twinCallId)) {
|
|
816
|
-
const combined = String(toolResultText(message?.content) ?? '');
|
|
817
|
-
// tool-exec composes `${patch}\n\n${header}\n${shell}` (or appends the
|
|
818
|
-
// skipped marker to a failed patch). Split on those exact seams.
|
|
819
|
-
const headerMatch = combined.match(/\n+--- post_shell( FAILED[^\n]*)? ---\n?/);
|
|
820
|
-
const skippedMatch = headerMatch ? null : combined.match(/\n+--- post_shell skipped: patch failed ---\s*$/);
|
|
821
|
-
let patchPart = combined;
|
|
822
|
-
let shellPart = null;
|
|
823
|
-
if (headerMatch) {
|
|
824
|
-
patchPart = combined.slice(0, headerMatch.index);
|
|
825
|
-
shellPart = combined.slice(headerMatch.index + headerMatch[0].length);
|
|
826
|
-
// Keep the FAILED banner on the shell card so its error state and
|
|
827
|
-
// fix-and-re-verify guidance survive the split.
|
|
828
|
-
if (headerMatch[1]) shellPart = `Error: post_shell FAILED (patch is applied; fix and re-verify)\n${shellPart}`;
|
|
829
|
-
} else if (skippedMatch) {
|
|
830
|
-
patchPart = combined.slice(0, skippedMatch.index);
|
|
831
|
-
shellPart = 'Error: post_shell skipped: patch failed';
|
|
832
|
-
}
|
|
833
|
-
if (shellPart !== null) {
|
|
834
|
-
flushToolResults([
|
|
835
|
-
{
|
|
836
|
-
role: 'tool',
|
|
837
|
-
tool_call_id: parentCallId,
|
|
838
|
-
content: patchPart,
|
|
839
|
-
...(Object.hasOwn(message || {}, 'uiDiff') ? { uiDiff: message.uiDiff } : {}),
|
|
840
|
-
},
|
|
841
|
-
{ role: 'tool', tool_call_id: twinCallId, content: shellPart },
|
|
842
|
-
], toolCards, cardByCallId, toolGroups, resultsDone);
|
|
843
|
-
return;
|
|
844
|
-
}
|
|
845
|
-
// Unsplittable (offloaded/preview-shaped result): parent keeps the
|
|
846
|
-
// combined text; settle the twin instead of leaving it Running.
|
|
847
|
-
flushToolResults([
|
|
848
|
-
message,
|
|
849
|
-
{ role: 'tool', tool_call_id: twinCallId, content: '(post_shell output included in the patch result above)' },
|
|
850
|
-
], toolCards, cardByCallId, toolGroups, resultsDone);
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
807
|
flushToolResults([message], toolCards, cardByCallId, toolGroups, resultsDone);
|
|
854
808
|
};
|
|
855
809
|
|
|
@@ -918,24 +872,7 @@ export function createRunTurn(bag) {
|
|
|
918
872
|
}
|
|
919
873
|
const batchCalls = (calls || []).filter(Boolean);
|
|
920
874
|
if (batchCalls.length === 0) return;
|
|
921
|
-
|
|
922
|
-
// synthetic trailing shell call, so the (possibly long) verify phase
|
|
923
|
-
// renders as its own Running command card in call order.
|
|
924
|
-
const displayCalls = [];
|
|
925
|
-
for (const c of batchCalls) {
|
|
926
|
-
displayCalls.push(c);
|
|
927
|
-
if (toolCallName(c) !== 'apply_patch') continue;
|
|
928
|
-
let patchCallArgs = toolCallArgs(c);
|
|
929
|
-
if (typeof patchCallArgs === 'string') {
|
|
930
|
-
try { patchCallArgs = JSON.parse(patchCallArgs); } catch { patchCallArgs = null; }
|
|
931
|
-
}
|
|
932
|
-
const postShellCmd = typeof patchCallArgs?.post_shell === 'string' ? patchCallArgs.post_shell.trim() : '';
|
|
933
|
-
const parentCallId = toolCallId(c);
|
|
934
|
-
if (!postShellCmd || !parentCallId) continue;
|
|
935
|
-
const twinCallId = `${parentCallId}::post_shell`;
|
|
936
|
-
postShellTwins.set(parentCallId, twinCallId);
|
|
937
|
-
displayCalls.push({ id: twinCallId, name: 'shell', args: { command: postShellCmd } });
|
|
938
|
-
}
|
|
875
|
+
const displayCalls = batchCalls;
|
|
939
876
|
const agentBatch = ++providerToolBatch;
|
|
940
877
|
const committedAssistantSegment = commitAssistantSegment({ sealToolBlock: true });
|
|
941
878
|
if (committedAssistantSegment) {
|
|
@@ -1230,8 +1167,11 @@ export function createRunTurn(bag) {
|
|
|
1230
1167
|
if (!isCurrentTurn() || watchdog.tripped) return;
|
|
1231
1168
|
if (String(chunk ?? '')) {
|
|
1232
1169
|
if (!markTurnProgress('reasoning-delta')) return;
|
|
1233
|
-
markPromptCommitted();
|
|
1234
1170
|
}
|
|
1171
|
+
// Reasoning is NOT a commit boundary (Claude Code parity): a turn
|
|
1172
|
+
// interrupted while it is still thinking produced no model-visible
|
|
1173
|
+
// message, so Esc must hand the prompt back to the input box. Text,
|
|
1174
|
+
// tool calls and turn end still commit it below.
|
|
1235
1175
|
startThinkingSegment();
|
|
1236
1176
|
thinkingText += String(chunk ?? '');
|
|
1237
1177
|
// Accumulate reasoning text; fire at most one render per STREAM_BATCH_INTERVAL_MS.
|
|
@@ -109,7 +109,7 @@ import { createSessionFlow } from './engine/session-flow.mjs';
|
|
|
109
109
|
import { createRunTurn } from './engine/turn.mjs';
|
|
110
110
|
import { createEngineApi } from './engine/session-api.mjs';
|
|
111
111
|
import { createFrameBatchedStorePublisher } from './engine/frame-batched-store.mjs';
|
|
112
|
-
import { createLiveShare, liveSharePipePath } from './engine/live-share.mjs';
|
|
112
|
+
import { createLiveShare, forwardViewerSubmit, liveSharePipePath } from './engine/live-share.mjs';
|
|
113
113
|
import { displayModelName } from '../ui/model-display.mjs';
|
|
114
114
|
|
|
115
115
|
const SESSION_RUNTIME_MODULE = '../mixdog-session-runtime.mjs';
|
|
@@ -132,6 +132,17 @@ export function preloadSessionRuntimeModule() {
|
|
|
132
132
|
});
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
let agentLoopPrewarmPromise = null;
|
|
136
|
+
export function preloadAgentLoopRuntime() {
|
|
137
|
+
agentLoopPrewarmPromise ??= import(
|
|
138
|
+
'../runtime/agent/orchestrator/session/manager/runtime-loaders.mjs'
|
|
139
|
+
).then((module) => module.prewarmAgentLoop());
|
|
140
|
+
void agentLoopPrewarmPromise.catch(() => {
|
|
141
|
+
// Reservation-time prewarm retries through the same shared loader.
|
|
142
|
+
agentLoopPrewarmPromise = null;
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
135
146
|
// Windows keychain reads go through a DPAPI PowerShell host whose cold start
|
|
136
147
|
// dominates a packaged boot (measured ~1.5s). The runtime batches every secret
|
|
137
148
|
// into one call, but it only starts that batch when the runtime is created —
|
|
@@ -948,38 +959,43 @@ export async function createLocalEngineSession({
|
|
|
948
959
|
// owner frames own stats/agent/tool state (see runtimePulseTimer above).
|
|
949
960
|
bag.liveShareMirroring = () => state.sessionRemoteAttached && liveShare.viewerConnected();
|
|
950
961
|
// Live viewer submits ride the owner's pipe (instant user bubble + shared
|
|
951
|
-
// streaming); the durable spool
|
|
962
|
+
// streaming); the durable spool remains the fallback. Returns null when this
|
|
963
|
+
// surface is NOT an attached viewer, so the local engine keeps the prompt.
|
|
964
|
+
const viewerSubmitIntake = (prompt, options = {}) => {
|
|
965
|
+
if (!state.sessionRemoteAttached) return null;
|
|
966
|
+
const text = String(promptDisplayText(prompt, options) || '').trim();
|
|
967
|
+
if (!text) return { accepted: false };
|
|
968
|
+
return {
|
|
969
|
+
accepted: forwardViewerSubmit({
|
|
970
|
+
text,
|
|
971
|
+
options,
|
|
972
|
+
share: liveShare,
|
|
973
|
+
// Writing to the owner's spool instead of starting a fake local turn
|
|
974
|
+
// that would render an error/synthetic assistant message here.
|
|
975
|
+
spool: (submissionId) => runtime.enqueueRemoteAttachedPrompt?.({
|
|
976
|
+
content: prompt,
|
|
977
|
+
text,
|
|
978
|
+
id: submissionId,
|
|
979
|
+
}) === true,
|
|
980
|
+
}),
|
|
981
|
+
};
|
|
982
|
+
};
|
|
952
983
|
if (typeof api.submit === 'function') {
|
|
953
984
|
const baseSubmit = api.submit;
|
|
954
985
|
api.submit = (prompt, options = {}) => {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
text: text,
|
|
969
|
-
id: submissionId,
|
|
970
|
-
}) === true;
|
|
971
|
-
// A pipe WRITE is not a delivery: the owner may refuse the prompt or
|
|
972
|
-
// the socket may die between write and read. sendSubmit stays
|
|
973
|
-
// synchronous (the store contract), but an unacknowledged submit is
|
|
974
|
-
// re-delivered through the durable spool — late, never lost.
|
|
975
|
-
if (text && liveShare.sendSubmit(text, {
|
|
976
|
-
id: submissionId,
|
|
977
|
-
submittedAt: options.submittedAt,
|
|
978
|
-
onUndelivered: spoolFallback,
|
|
979
|
-
})) return true;
|
|
980
|
-
return spoolFallback();
|
|
981
|
-
}
|
|
982
|
-
return baseSubmit(prompt, options);
|
|
986
|
+
const forwarded = viewerSubmitIntake(prompt, options);
|
|
987
|
+
return forwarded ? forwarded.accepted : baseSubmit(prompt, options);
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
// The daemon's intake boundary is submitAsync (engine-daemon-service prefers
|
|
991
|
+
// it over submit). Leaving it unwrapped let a daemon-hosted VIEWER — the
|
|
992
|
+
// desktop pane on a session the terminal owns — queue the prompt locally, so
|
|
993
|
+
// its own user row stood beside the owner's mirrored twin.
|
|
994
|
+
if (typeof api.submitAsync === 'function') {
|
|
995
|
+
const baseSubmitAsync = api.submitAsync;
|
|
996
|
+
api.submitAsync = async (prompt, options = {}) => {
|
|
997
|
+
const forwarded = viewerSubmitIntake(prompt, options);
|
|
998
|
+
return forwarded ? forwarded.accepted : baseSubmitAsync(prompt, options);
|
|
983
999
|
};
|
|
984
1000
|
}
|
|
985
1001
|
// Viewer stop button: the local engine has no in-flight turn to cancel —
|
|
@@ -1010,7 +1026,7 @@ export async function createLocalEngineSession({
|
|
|
1010
1026
|
reconcileLiveShareNow();
|
|
1011
1027
|
if (method === 'resume' && result === true && state.sessionRemoteAttached) {
|
|
1012
1028
|
const id = String(state.sessionId || '');
|
|
1013
|
-
//
|
|
1029
|
+
// The session projection holds renderer publications across resume. Keep that hold
|
|
1014
1030
|
// until the owner's first FULL frame replaces the persisted transcript,
|
|
1015
1031
|
// then synchronously publish the complete draft before getState().
|
|
1016
1032
|
// A healthy local owner serves its full frame on connect within tens
|
package/src/tui/index.jsx
CHANGED
|
@@ -30,7 +30,6 @@ import { POP_KITTY, DISABLE_MODIFY_OTHER_KEYS } from './keyboard-protocol.mjs';
|
|
|
30
30
|
import { displayWidth } from './display-width.mjs';
|
|
31
31
|
import { rotateBoundedLog, PLUGIN_LOG_MAX_BYTES, PLUGIN_LOG_KEEP_BYTES } from '../lib/mixdog-debug.cjs';
|
|
32
32
|
import { localPackageVersion } from '../runtime/shared/update-checker.mjs';
|
|
33
|
-
import { touchProjectSelected } from '../standalone/projects.mjs';
|
|
34
33
|
|
|
35
34
|
// Trailing `\x1b[>0s` restores XTSHIFTESCAPE (shift-to-select-extend) to its
|
|
36
35
|
// terminal default; MOUSE_TRACKING_ON opts into `\x1b[>1s`, so every mouse/alt
|
|
@@ -517,7 +516,7 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
|
|
|
517
516
|
// this, a project entered straight from the shell never moved in the
|
|
518
517
|
// picker's most-recent-first order. Unregistered folders are ignored.
|
|
519
518
|
try {
|
|
520
|
-
touchProjectSelected(store.getState?.()?.cwd || process.cwd());
|
|
519
|
+
await store.touchProjectSelected(store.getState?.()?.cwd || process.cwd());
|
|
521
520
|
} catch { /* best-effort recency */ }
|
|
522
521
|
// The palette lives in THIS process: on a daemon-backed store the engine's
|
|
523
522
|
// theme methods would run inside the daemon, so the picker saw a promise
|
package/src/tui/spinner-meta.mjs
CHANGED
|
@@ -9,9 +9,6 @@
|
|
|
9
9
|
* for 30s. One builder, one answer.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
/** Short turns stay quiet: elapsed/token meta only after this long. */
|
|
13
|
-
export const SHOW_TOKENS_AFTER_MS = 30_000;
|
|
14
|
-
|
|
15
12
|
const compactNumberFormatter = new Intl.NumberFormat('en-US', {
|
|
16
13
|
notation: 'compact',
|
|
17
14
|
maximumFractionDigits: 1,
|
|
@@ -62,7 +59,10 @@ export function buildSpinnerMeta({
|
|
|
62
59
|
const thinkingText = spinnerThinkingLabel({ thinking, thinkingSince, thinkingMs, effort });
|
|
63
60
|
return {
|
|
64
61
|
tokensText: tokens > 0 ? `${formatSpinnerTokens(tokens)} tokens` : '',
|
|
65
|
-
|
|
62
|
+
// The count appears with the FIRST token (user: 30초가 아니라 바로).
|
|
63
|
+
// Elapsed was never gated, so the byline used to grow a second field
|
|
64
|
+
// mid-turn for no reason the reader could see; now both arrive together.
|
|
65
|
+
showTokens: tokens > 0,
|
|
66
66
|
thinkingText,
|
|
67
67
|
thinkingActive: Boolean(thinking || Number(thinkingSince) > 0),
|
|
68
68
|
};
|