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
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
function positiveInt(value, fallback) {
|
|
2
|
+
const parsed = Math.floor(Number(value));
|
|
3
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function activeLimit(value, fallback) {
|
|
7
|
+
if (value === null || value === Infinity) return Infinity;
|
|
8
|
+
return positiveInt(value, fallback);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function schedulerError(message, statusCode = 503) {
|
|
12
|
+
const error = new Error(message);
|
|
13
|
+
error.statusCode = statusCode;
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function abortError(signal, fallback = 'scheduled call canceled') {
|
|
18
|
+
return signal?.reason instanceof Error
|
|
19
|
+
? signal.reason
|
|
20
|
+
: schedulerError(String(signal?.reason || fallback), 499);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Owner-fair async dispatcher with an optional active limit.
|
|
25
|
+
*
|
|
26
|
+
* One owner may borrow the entire queue while it is alone. When another owner
|
|
27
|
+
* appears, new admissions are divided by weight and a completely full queue
|
|
28
|
+
* evicts one tail item from the largest borrower so the newcomer always gets a
|
|
29
|
+
* seat. Running work is never revoked.
|
|
30
|
+
*/
|
|
31
|
+
export function createFairCallScheduler({
|
|
32
|
+
name = 'daemon call',
|
|
33
|
+
activeMax = 32,
|
|
34
|
+
queueMax = 256,
|
|
35
|
+
minOwnerQueue = 8,
|
|
36
|
+
dispatchBurst = 8,
|
|
37
|
+
yieldUnbounded = false,
|
|
38
|
+
schedule = setImmediate,
|
|
39
|
+
now = Date.now,
|
|
40
|
+
} = {}) {
|
|
41
|
+
const maxActive = activeLimit(activeMax, 32);
|
|
42
|
+
const maxQueued = positiveInt(queueMax, 256);
|
|
43
|
+
const ownerFloor = Math.max(1, Math.min(maxQueued, positiveInt(minOwnerQueue, 8)));
|
|
44
|
+
const maxBurst = positiveInt(dispatchBurst, 8);
|
|
45
|
+
const groups = new Map();
|
|
46
|
+
let active = 0;
|
|
47
|
+
let queued = 0;
|
|
48
|
+
let scheduled = false;
|
|
49
|
+
let closed = false;
|
|
50
|
+
|
|
51
|
+
function ownerId(value) {
|
|
52
|
+
const clean = String(value || '').trim();
|
|
53
|
+
return clean ? clean.slice(0, 240) : 'anonymous';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function groupFor(owner, weight = 1) {
|
|
57
|
+
const id = ownerId(owner);
|
|
58
|
+
let group = groups.get(id);
|
|
59
|
+
if (!group) {
|
|
60
|
+
group = {
|
|
61
|
+
owner: id,
|
|
62
|
+
weight: positiveInt(weight, 1),
|
|
63
|
+
current: 0,
|
|
64
|
+
active: 0,
|
|
65
|
+
queue: [],
|
|
66
|
+
};
|
|
67
|
+
groups.set(id, group);
|
|
68
|
+
} else if (weight !== undefined) {
|
|
69
|
+
group.weight = positiveInt(weight, group.weight);
|
|
70
|
+
}
|
|
71
|
+
return group;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function maybeDeleteGroup(group) {
|
|
75
|
+
if (group && group.active === 0 && group.queue.length === 0) groups.delete(group.owner);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function detach(item) {
|
|
79
|
+
if (!item?.onAbort || !item.signal) return;
|
|
80
|
+
try { item.signal.removeEventListener('abort', item.onAbort); } catch {}
|
|
81
|
+
item.onAbort = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function queuedGroups(extra = null) {
|
|
85
|
+
const out = [];
|
|
86
|
+
for (const group of groups.values()) {
|
|
87
|
+
if (group.queue.length > 0 || group === extra) out.push(group);
|
|
88
|
+
}
|
|
89
|
+
if (extra && !out.includes(extra)) out.push(extra);
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function rejectBorrowedTail(incoming) {
|
|
94
|
+
let borrower = null;
|
|
95
|
+
for (const group of groups.values()) {
|
|
96
|
+
if (group === incoming || group.queue.length <= ownerFloor) continue;
|
|
97
|
+
if (!borrower || group.queue.length > borrower.queue.length) borrower = group;
|
|
98
|
+
}
|
|
99
|
+
if (!borrower) return false;
|
|
100
|
+
const displaced = borrower.queue.pop();
|
|
101
|
+
queued = Math.max(0, queued - 1);
|
|
102
|
+
detach(displaced);
|
|
103
|
+
displaced.reject(schedulerError(
|
|
104
|
+
`${name} queue rebalanced for another client; retry after running work completes`,
|
|
105
|
+
503,
|
|
106
|
+
));
|
|
107
|
+
maybeDeleteGroup(borrower);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function fairQueueLimit(group) {
|
|
112
|
+
const contenders = queuedGroups(group);
|
|
113
|
+
if (contenders.length <= 1) return maxQueued;
|
|
114
|
+
const totalWeight = contenders.reduce((sum, candidate) => sum + candidate.weight, 0);
|
|
115
|
+
return Math.max(
|
|
116
|
+
ownerFloor,
|
|
117
|
+
Math.floor(maxQueued * group.weight / Math.max(1, totalWeight)),
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function pickGroup() {
|
|
122
|
+
const ready = [...groups.values()].filter((group) => group.queue.length > 0);
|
|
123
|
+
if (ready.length === 0) return null;
|
|
124
|
+
const totalWeight = ready.reduce((sum, group) => sum + group.weight, 0);
|
|
125
|
+
let chosen = null;
|
|
126
|
+
for (const group of ready) {
|
|
127
|
+
group.current += group.weight;
|
|
128
|
+
if (!chosen || group.current > chosen.current) chosen = group;
|
|
129
|
+
}
|
|
130
|
+
chosen.current -= totalWeight;
|
|
131
|
+
return chosen;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function scheduleDispatch() {
|
|
135
|
+
if (closed || scheduled || active >= maxActive || queued === 0) return;
|
|
136
|
+
scheduled = true;
|
|
137
|
+
schedule(() => {
|
|
138
|
+
scheduled = false;
|
|
139
|
+
let started = 0;
|
|
140
|
+
while (!closed && active < maxActive && queued > 0 && started < maxBurst) {
|
|
141
|
+
const group = pickGroup();
|
|
142
|
+
const item = group?.queue.shift();
|
|
143
|
+
if (!group || !item) break;
|
|
144
|
+
queued = Math.max(0, queued - 1);
|
|
145
|
+
detach(item);
|
|
146
|
+
active += 1;
|
|
147
|
+
group.active += 1;
|
|
148
|
+
started += 1;
|
|
149
|
+
Promise.resolve()
|
|
150
|
+
.then(item.run)
|
|
151
|
+
.then(item.resolve, item.reject)
|
|
152
|
+
.finally(() => {
|
|
153
|
+
active = Math.max(0, active - 1);
|
|
154
|
+
group.active = Math.max(0, group.active - 1);
|
|
155
|
+
maybeDeleteGroup(group);
|
|
156
|
+
scheduleDispatch();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// A bounded burst amortizes loopback dispatch without letting synchronous
|
|
160
|
+
// setup monopolize the supervisor's socket/control-plane turn.
|
|
161
|
+
scheduleDispatch();
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function enqueue(owner, run, { weight = 1, signal = null } = {}) {
|
|
166
|
+
if (closed) return Promise.reject(schedulerError(`${name} scheduler is closed`));
|
|
167
|
+
if (typeof run !== 'function') return Promise.reject(new TypeError('scheduled call must be a function'));
|
|
168
|
+
if (signal?.aborted) return Promise.reject(abortError(signal));
|
|
169
|
+
const group = groupFor(owner, weight);
|
|
170
|
+
// Default daemon lanes are unbounded. Do not enqueue them only to bounce
|
|
171
|
+
// through setImmediate: independent loopback calls start in this microtask,
|
|
172
|
+
// while finite operator-configured lanes retain the fair queue below.
|
|
173
|
+
if (maxActive === Infinity && !yieldUnbounded && queued === 0) {
|
|
174
|
+
active += 1;
|
|
175
|
+
group.active += 1;
|
|
176
|
+
return Promise.resolve()
|
|
177
|
+
.then(run)
|
|
178
|
+
.finally(() => {
|
|
179
|
+
active = Math.max(0, active - 1);
|
|
180
|
+
group.active = Math.max(0, group.active - 1);
|
|
181
|
+
maybeDeleteGroup(group);
|
|
182
|
+
scheduleDispatch();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const hasCompetitor = queuedGroups(group).some((candidate) =>
|
|
186
|
+
candidate !== group && candidate.queue.length > 0);
|
|
187
|
+
if (hasCompetitor && group.queue.length >= fairQueueLimit(group)) {
|
|
188
|
+
return Promise.reject(schedulerError(
|
|
189
|
+
`${name} client queue is full; retry after this client's running work completes`,
|
|
190
|
+
429,
|
|
191
|
+
));
|
|
192
|
+
}
|
|
193
|
+
if (queued >= maxQueued && !rejectBorrowedTail(group)) {
|
|
194
|
+
return Promise.reject(schedulerError(`${name} queue is full`, 503));
|
|
195
|
+
}
|
|
196
|
+
let item;
|
|
197
|
+
const promise = new Promise((resolve, reject) => {
|
|
198
|
+
item = {
|
|
199
|
+
run,
|
|
200
|
+
resolve,
|
|
201
|
+
reject,
|
|
202
|
+
signal,
|
|
203
|
+
onAbort: null,
|
|
204
|
+
queuedAt: now(),
|
|
205
|
+
};
|
|
206
|
+
if (signal) {
|
|
207
|
+
item.onAbort = () => {
|
|
208
|
+
const index = group.queue.indexOf(item);
|
|
209
|
+
if (index < 0) return;
|
|
210
|
+
group.queue.splice(index, 1);
|
|
211
|
+
queued = Math.max(0, queued - 1);
|
|
212
|
+
detach(item);
|
|
213
|
+
maybeDeleteGroup(group);
|
|
214
|
+
reject(abortError(signal));
|
|
215
|
+
};
|
|
216
|
+
signal.addEventListener('abort', item.onAbort, { once: true });
|
|
217
|
+
}
|
|
218
|
+
group.queue.push(item);
|
|
219
|
+
queued += 1;
|
|
220
|
+
});
|
|
221
|
+
scheduleDispatch();
|
|
222
|
+
return promise;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function close(reason = `${name} scheduler is closed`) {
|
|
226
|
+
if (closed) return;
|
|
227
|
+
closed = true;
|
|
228
|
+
const error = schedulerError(reason);
|
|
229
|
+
for (const group of groups.values()) {
|
|
230
|
+
for (const item of group.queue.splice(0)) {
|
|
231
|
+
detach(item);
|
|
232
|
+
item.reject(error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
queued = 0;
|
|
236
|
+
for (const group of [...groups.values()]) maybeDeleteGroup(group);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function snapshot() {
|
|
240
|
+
let oldestQueuedAt = Infinity;
|
|
241
|
+
for (const group of groups.values()) {
|
|
242
|
+
for (const item of group.queue) {
|
|
243
|
+
oldestQueuedAt = Math.min(oldestQueuedAt, Number(item.queuedAt) || Infinity);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
active,
|
|
248
|
+
queued,
|
|
249
|
+
owners: [...groups.values()].filter((group) => group.active || group.queue.length).length,
|
|
250
|
+
activeMax: Number.isFinite(maxActive) ? maxActive : null,
|
|
251
|
+
dispatchBurst: maxBurst,
|
|
252
|
+
queueMax: maxQueued,
|
|
253
|
+
oldestWaitMs: Number.isFinite(oldestQueuedAt) ? Math.max(0, now() - oldestQueuedAt) : 0,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
enqueue,
|
|
259
|
+
close,
|
|
260
|
+
snapshot,
|
|
261
|
+
get active() { return active; },
|
|
262
|
+
get queued() { return queued; },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit session protocol shared by every Mixdog client.
|
|
3
|
+
*
|
|
4
|
+
* The wire exposes session lifetime operations directly. Engine capabilities
|
|
5
|
+
* are finite and split into read/configure lanes; clients cannot discover or
|
|
6
|
+
* invoke arbitrary properties on the daemon-hosted runtime.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const SESSION_READ_ACTIONS = Object.freeze([
|
|
10
|
+
'contextStatus',
|
|
11
|
+
'getAgentDefinition',
|
|
12
|
+
'getAutoClear',
|
|
13
|
+
'getChannelSettings',
|
|
14
|
+
'getChannelSetup',
|
|
15
|
+
'getChannelWorkerStatus',
|
|
16
|
+
'getCompactionSettings',
|
|
17
|
+
'getDisabledSkills',
|
|
18
|
+
'getMediaJob',
|
|
19
|
+
'getOnboardingStatus',
|
|
20
|
+
'getOutputStyle',
|
|
21
|
+
'getProfile',
|
|
22
|
+
'getProviderSetup',
|
|
23
|
+
'getRecapSettings',
|
|
24
|
+
'getSearchRoute',
|
|
25
|
+
'getSettingsSnapshot',
|
|
26
|
+
'getSystemShell',
|
|
27
|
+
'getTheme',
|
|
28
|
+
'getTurnReviewDiff',
|
|
29
|
+
'getUpdateSettings',
|
|
30
|
+
'getUpdateStatus',
|
|
31
|
+
'getUsageDashboard',
|
|
32
|
+
'getVoiceStatus',
|
|
33
|
+
'getWorkflowPack',
|
|
34
|
+
'hooksStatus',
|
|
35
|
+
'isRemoteEnabled',
|
|
36
|
+
'listAgents',
|
|
37
|
+
'listMediaAssets',
|
|
38
|
+
'listMediaLanes',
|
|
39
|
+
'listOutputStyles',
|
|
40
|
+
'listPresets',
|
|
41
|
+
'listProviderModels',
|
|
42
|
+
'listProviders',
|
|
43
|
+
'listSearchModels',
|
|
44
|
+
'listThemes',
|
|
45
|
+
'listWorkflows',
|
|
46
|
+
'mcpStatus',
|
|
47
|
+
'peekSessionTranscript',
|
|
48
|
+
'pluginsStatus',
|
|
49
|
+
'readMediaAsset',
|
|
50
|
+
'resolveMediaFile',
|
|
51
|
+
'skillContent',
|
|
52
|
+
'skillsStatus',
|
|
53
|
+
'toolsStatus',
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
|
|
57
|
+
'addHookRule',
|
|
58
|
+
'addMcpServer',
|
|
59
|
+
'addPlugin',
|
|
60
|
+
'addSkill',
|
|
61
|
+
'agentControl',
|
|
62
|
+
'authenticateProvider',
|
|
63
|
+
'beginOAuthProviderLogin',
|
|
64
|
+
'cacheMediaThumbnail',
|
|
65
|
+
'cancelMediaJob',
|
|
66
|
+
'checkForUpdate',
|
|
67
|
+
'claimRemote',
|
|
68
|
+
'clear',
|
|
69
|
+
'compact',
|
|
70
|
+
'completeOnboarding',
|
|
71
|
+
'createWorkflow',
|
|
72
|
+
'deleteAgentDefinition',
|
|
73
|
+
'deleteHookRule',
|
|
74
|
+
'deleteMediaAsset',
|
|
75
|
+
'deleteSchedule',
|
|
76
|
+
'deleteSession',
|
|
77
|
+
'deleteWebhook',
|
|
78
|
+
'deleteWorkflow',
|
|
79
|
+
'enablePluginMcp',
|
|
80
|
+
'forgetDiscordToken',
|
|
81
|
+
'forgetProviderAuth',
|
|
82
|
+
'forgetTelegramToken',
|
|
83
|
+
'loginOAuthProvider',
|
|
84
|
+
'loginOpenCodeGoUsage',
|
|
85
|
+
'memoryControl',
|
|
86
|
+
'openMediaAsset',
|
|
87
|
+
'openMediaFolder',
|
|
88
|
+
'pushNotice',
|
|
89
|
+
'recall',
|
|
90
|
+
'reconnectMcp',
|
|
91
|
+
'releaseRemote',
|
|
92
|
+
'reloadPlugins',
|
|
93
|
+
'reloadSkills',
|
|
94
|
+
'rememberPromptHistory',
|
|
95
|
+
'removeMcpServer',
|
|
96
|
+
'removeNotice',
|
|
97
|
+
'removePlugin',
|
|
98
|
+
'renameSessionTitle',
|
|
99
|
+
'restoreNewerTranscript',
|
|
100
|
+
'restoreOlderTranscript',
|
|
101
|
+
'restoreQueued',
|
|
102
|
+
'revertTurnReviewFile',
|
|
103
|
+
'rewindToItem',
|
|
104
|
+
'runDoctor',
|
|
105
|
+
'runScheduleNow',
|
|
106
|
+
'runUpdateNow',
|
|
107
|
+
'saveAgentDefinition',
|
|
108
|
+
'saveDiscordToken',
|
|
109
|
+
'saveOpenAIUsageSessionKey',
|
|
110
|
+
'saveOpenCodeGoUsageAuth',
|
|
111
|
+
'saveProviderApiKey',
|
|
112
|
+
'saveSchedule',
|
|
113
|
+
'saveTelegramToken',
|
|
114
|
+
'saveWebhook',
|
|
115
|
+
'saveWorkflowPack',
|
|
116
|
+
'selectTools',
|
|
117
|
+
'setAgentRoute',
|
|
118
|
+
'setAutoClear',
|
|
119
|
+
'setAutoUpdate',
|
|
120
|
+
'setBackend',
|
|
121
|
+
'setChannel',
|
|
122
|
+
'setChannelsEnabled',
|
|
123
|
+
'setCompactionSettings',
|
|
124
|
+
'setCwd',
|
|
125
|
+
'setDisabledSkills',
|
|
126
|
+
'setEffort',
|
|
127
|
+
'setFast',
|
|
128
|
+
'setHookRuleEnabled',
|
|
129
|
+
'setLocalProvider',
|
|
130
|
+
'setMcpServerEnabled',
|
|
131
|
+
'setModel',
|
|
132
|
+
'setOutputStyle',
|
|
133
|
+
'setProfile',
|
|
134
|
+
'setProgressHint',
|
|
135
|
+
'setRecapEnabled',
|
|
136
|
+
'setRoute',
|
|
137
|
+
'setScheduleEnabled',
|
|
138
|
+
'setSearchRoute',
|
|
139
|
+
'setSystemShell',
|
|
140
|
+
'setTheme',
|
|
141
|
+
'setToolMode',
|
|
142
|
+
'setWebhookConfig',
|
|
143
|
+
'setWebhookEnabled',
|
|
144
|
+
'setWorkflow',
|
|
145
|
+
'skipOnboarding',
|
|
146
|
+
'startMediaJob',
|
|
147
|
+
'switchContext',
|
|
148
|
+
'toggleFast',
|
|
149
|
+
'toggleRemote',
|
|
150
|
+
'toggleVoice',
|
|
151
|
+
'transcribeAudio',
|
|
152
|
+
'updatePlugin',
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
export const SESSION_READ_ACTION_SET = new Set(SESSION_READ_ACTIONS);
|
|
156
|
+
export const SESSION_CONFIGURE_ACTION_SET = new Set(SESSION_CONFIGURE_ACTIONS);
|
|
157
|
+
|
|
158
|
+
function requireTransport(transport) {
|
|
159
|
+
if (!transport || typeof transport.call !== 'function') {
|
|
160
|
+
throw new TypeError('session transport is invalid');
|
|
161
|
+
}
|
|
162
|
+
return transport;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Build the one finite client used by TUI, desktop, and in-daemon adapters. */
|
|
166
|
+
export function createSessionProtocolClient(rawTransport) {
|
|
167
|
+
const transport = requireTransport(rawTransport);
|
|
168
|
+
const route = (name, params = {}, options = {}) => transport.call(name, params, options);
|
|
169
|
+
return Object.freeze({
|
|
170
|
+
list: (params = {}, options = {}) => route('session.list', params, options),
|
|
171
|
+
create: (params = {}, options = {}) => route('session.create', params, options),
|
|
172
|
+
read: (params = {}, options = {}) => route('session.read', params, options),
|
|
173
|
+
subscribe: (params = {}, options = {}) => route('session.subscribe', params, options),
|
|
174
|
+
unsubscribe: (params = {}, options = {}) => route('session.unsubscribe', params, options),
|
|
175
|
+
submit: (params = {}, options = {}) => route('session.submit', params, options),
|
|
176
|
+
abort: (params = {}, options = {}) => route('session.abort', params, options),
|
|
177
|
+
approve: (params = {}, options = {}) => route('session.approve', params, options),
|
|
178
|
+
configure: (params = {}, options = {}) => route('session.configure', params, options),
|
|
179
|
+
projectList: (params = {}, options = {}) => route('project.list', params, options),
|
|
180
|
+
projectInspect: (params = {}, options = {}) => route('project.inspect', params, options),
|
|
181
|
+
projectAdd: (params = {}, options = {}) => route('project.add', params, options),
|
|
182
|
+
projectTouch: (params = {}, options = {}) => route('project.touch', params, options),
|
|
183
|
+
projectRename: (params = {}, options = {}) => route('project.rename', params, options),
|
|
184
|
+
projectRemove: (params = {}, options = {}) => route('project.remove', params, options),
|
|
185
|
+
projectEnsureDirectory: (params = {}, options = {}) => route('project.ensureDirectory', params, options),
|
|
186
|
+
close: (reason) => Promise.resolve(transport.close?.(reason)),
|
|
187
|
+
});
|
|
188
|
+
}
|
package/src/tui/App.jsx
CHANGED
|
@@ -42,16 +42,6 @@ import {
|
|
|
42
42
|
splitPastedImagePathCandidates,
|
|
43
43
|
} from './paste-attachments.mjs';
|
|
44
44
|
import { formatDuration } from './time-format.mjs';
|
|
45
|
-
import {
|
|
46
|
-
listProjects,
|
|
47
|
-
addProject,
|
|
48
|
-
touchProjectSelected,
|
|
49
|
-
renameProject,
|
|
50
|
-
isDirectory,
|
|
51
|
-
pathExists,
|
|
52
|
-
ensureDir,
|
|
53
|
-
resolveProjectPath,
|
|
54
|
-
} from '../standalone/projects.mjs';
|
|
55
45
|
import { pickFolder } from '../standalone/folder-dialog.mjs';
|
|
56
46
|
import {
|
|
57
47
|
formatHookDenialDetail,
|
|
@@ -100,6 +90,7 @@ import { usePromptDraftFlow } from './app/use-prompt-draft-flow.mjs';
|
|
|
100
90
|
import { useTranscriptActivity } from './app/use-transcript-activity.mjs';
|
|
101
91
|
import { createAppPickers } from './app/create-app-pickers.mjs';
|
|
102
92
|
import { usePromptQueueHistory } from './app/use-prompt-queue-history.mjs';
|
|
93
|
+
import { useMessageSelector } from './app/message-selector.mjs';
|
|
103
94
|
import { useTerminalChrome } from './app/use-terminal-chrome.mjs';
|
|
104
95
|
import { useTranscriptWindow } from './app/use-transcript-window.mjs';
|
|
105
96
|
import {
|
|
@@ -120,6 +111,7 @@ import {
|
|
|
120
111
|
transcriptMeasuredRowsCache,
|
|
121
112
|
buildTranscriptRowIndex,
|
|
122
113
|
transcriptRenderWindow,
|
|
114
|
+
transcriptSwapReturnsToTail,
|
|
123
115
|
} from './app/transcript-window.mjs';
|
|
124
116
|
import {
|
|
125
117
|
SEARCH_DEFAULT_ROUTE,
|
|
@@ -326,6 +318,9 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
|
|
|
326
318
|
// same glide back to the bottom.
|
|
327
319
|
const followingRef = useRef(false);
|
|
328
320
|
const lastItemsCountRef = useRef(0);
|
|
321
|
+
// Head item of the last committed transcript: a bulk swap (session load /
|
|
322
|
+
// clear / compaction trim) changes it, a live append never does.
|
|
323
|
+
const lastFirstItemIdRef = useRef(null);
|
|
329
324
|
// picker = null | { type, title, items, onSelect }
|
|
330
325
|
// Rendered as an option panel attached directly above the bottom prompt.
|
|
331
326
|
const pickerOpenedFromEnterRef = useRef(false);
|
|
@@ -413,10 +408,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
|
|
|
413
408
|
setSettingsPrompt,
|
|
414
409
|
setContextPanel,
|
|
415
410
|
closeUsagePanel,
|
|
416
|
-
listProjects,
|
|
417
|
-
addProject,
|
|
418
|
-
touchProjectSelected,
|
|
419
|
-
resolveProjectPath,
|
|
420
411
|
projectNameFromPath,
|
|
421
412
|
pickFolder,
|
|
422
413
|
});
|
|
@@ -429,9 +420,13 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
|
|
|
429
420
|
initialPickerBuiltRef.current = true;
|
|
430
421
|
const onboardingOwnsScreen = !resolveOnboardingCompleted(store, onboardingCompleted) || forceOnboarding;
|
|
431
422
|
if (!onboardingOwnsScreen && state.items.length === 0) {
|
|
432
|
-
setPicker(projectPicker.buildProjectPickerState({ initialEntry: true }));
|
|
423
|
+
setPicker(projectPicker.buildProjectPickerState({ initialEntry: true, loading: true }));
|
|
433
424
|
}
|
|
434
425
|
}
|
|
426
|
+
useEffect(() => {
|
|
427
|
+
if (livePickerRef.current?._projectInitialPending !== true) return;
|
|
428
|
+
void projectPickerRef.current?.openProjectPicker({ initialEntry: true });
|
|
429
|
+
}, [store]);
|
|
435
430
|
const {
|
|
436
431
|
beginNewProject,
|
|
437
432
|
registerProject,
|
|
@@ -793,12 +788,17 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
|
|
|
793
788
|
const count = state.items.length;
|
|
794
789
|
const previousCount = lastItemsCountRef.current;
|
|
795
790
|
lastItemsCountRef.current = count;
|
|
796
|
-
|
|
791
|
+
const firstId = count > 0 ? (state.items[0]?.id ?? null) : null;
|
|
792
|
+
const previousFirstId = lastFirstItemIdRef.current;
|
|
793
|
+
lastFirstItemIdRef.current = firstId;
|
|
794
|
+
// A bulk swap (compaction included) is an explicit return to the tail: the
|
|
795
|
+
// rows the reader was anchored to no longer exist.
|
|
796
|
+
if (transcriptSwapReturnsToTail({ count, previousCount, firstId, previousFirstId })) {
|
|
797
797
|
resetTranscriptScroll();
|
|
798
798
|
return;
|
|
799
799
|
}
|
|
800
800
|
if (count === previousCount || dragRef.current.active) return;
|
|
801
|
-
}, [state.items
|
|
801
|
+
}, [state.items, resetTranscriptScroll]);
|
|
802
802
|
|
|
803
803
|
// Exit + queued-restore + prompt history: app/use-prompt-queue-history.mjs.
|
|
804
804
|
const {
|
|
@@ -823,6 +823,17 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
|
|
|
823
823
|
promptHistoryNavRef,
|
|
824
824
|
});
|
|
825
825
|
|
|
826
|
+
// Double-Esc message selector (Claude Code parity): app/message-selector.mjs.
|
|
827
|
+
const { hasUserMessages, openMessageSelector } = useMessageSelector({
|
|
828
|
+
store,
|
|
829
|
+
state,
|
|
830
|
+
setPicker,
|
|
831
|
+
setPromptDraftOverride,
|
|
832
|
+
syncPromptLayoutRows,
|
|
833
|
+
showPromptHint,
|
|
834
|
+
clearPromptHint,
|
|
835
|
+
});
|
|
836
|
+
|
|
826
837
|
// PROMPT HANDLER cluster extracted to app/use-prompt-handlers.mjs.
|
|
827
838
|
const {
|
|
828
839
|
handlePromptPaste,
|
|
@@ -849,6 +860,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
|
|
|
849
860
|
recentPromptHistory,
|
|
850
861
|
resetPromptHistoryNav,
|
|
851
862
|
restoreQueuedToPrompt,
|
|
863
|
+
openMessageSelector,
|
|
852
864
|
usagePanel,
|
|
853
865
|
closeUsagePanel,
|
|
854
866
|
contextPanel,
|
|
@@ -1257,6 +1269,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
|
|
|
1257
1269
|
handlePromptHistoryNavigate,
|
|
1258
1270
|
handlePromptInterrupt,
|
|
1259
1271
|
handlePromptPaste,
|
|
1272
|
+
hasUserMessages,
|
|
1260
1273
|
hookPrompt,
|
|
1261
1274
|
initialStatusLine,
|
|
1262
1275
|
onPromptDraftChange,
|
package/src/tui/app/app-view.jsx
CHANGED
|
@@ -20,7 +20,7 @@ import { UsagePanel } from '../components/UsagePanel.jsx';
|
|
|
20
20
|
import { TextEntryPanel } from '../components/TextEntryPanel.jsx';
|
|
21
21
|
import { Item } from '../components/TranscriptItem.jsx';
|
|
22
22
|
export function renderAppView(ctx) {
|
|
23
|
-
const { PANEL_MAX_VISIBLE, acceptSlashPalette, activeSlashQuery, activeTools, agentRevision, cancelChannelPrompt, cancelHookPrompt, cancelProviderPrompt, cancelSettingsPrompt, cancelSlashPalette, channelPrompt, clearPromptHint, completeSlashPalette, contextPanel, cycleWorkflowFromPrompt, exiting, expandedOptionPanel, floatingPanelRows, frameColumns, gridSelectionActiveRef, guardHintWidth, handlePromptEscape, handlePromptHistoryNavigate, handlePromptInterrupt, handlePromptPaste, hookPrompt, initialStatusLine, inputBoxHidden, inputHint, inputHintTone, liveSpinner, onPromptDraftChange, onSubmit, overlayHintAttachItemIndex, overlayHintBandRows, overlayHintFallbackRow, overlayHintOnLastItem, panelCloseMaskRows, panelInkMaskEpoch, panelTransitionClearRows, panelTransitionEpoch, picker, pickerOpenedFromEnterRef, pickerOpenedFromEnterTimerRef, pickerVisibleRows, promptBoxRectRef, promptBoxRows, promptDraft, promptDraftOverride, promptMetaVisible, promptMouseSelectionRef, promptSelectionRef, promptSpinnerColumns, promptValueRef, providerPrompt, queuedCompact, queuedVisible, renderedTranscriptItems, resizeEpoch, resizeState, restoreQueuedToPrompt, setPicker, setSlashIndex, setTextEntryLayoutRows, settingsPrompt, showWelcomeBanner, slashCommands, slashIndex, slashPaletteOpen, state, statuslineStats, store, toolApproval, toolOutputExpanded, transcriptContentHeight, transcriptGuardRows, transcriptMeasureRef, transcriptTailPinned, transcriptWindow, transientStatusWidth, tuiReady, usagePanel, viewportHeight, welcomePromptHintRows, welcomePromptHintText } = ctx; /* DESTRUCTURE */
|
|
23
|
+
const { PANEL_MAX_VISIBLE, acceptSlashPalette, activeSlashQuery, activeTools, agentRevision, cancelChannelPrompt, cancelHookPrompt, cancelProviderPrompt, cancelSettingsPrompt, cancelSlashPalette, channelPrompt, clearPromptHint, completeSlashPalette, contextPanel, cycleWorkflowFromPrompt, exiting, expandedOptionPanel, floatingPanelRows, frameColumns, gridSelectionActiveRef, guardHintWidth, handlePromptEscape, handlePromptHistoryNavigate, handlePromptInterrupt, handlePromptPaste, hasUserMessages, hookPrompt, initialStatusLine, inputBoxHidden, inputHint, inputHintTone, liveSpinner, onPromptDraftChange, onSubmit, overlayHintAttachItemIndex, overlayHintBandRows, overlayHintFallbackRow, overlayHintOnLastItem, panelCloseMaskRows, panelInkMaskEpoch, panelTransitionClearRows, panelTransitionEpoch, picker, pickerOpenedFromEnterRef, pickerOpenedFromEnterTimerRef, pickerVisibleRows, promptBoxRectRef, promptBoxRows, promptDraft, promptDraftOverride, promptMetaVisible, promptMouseSelectionRef, promptSelectionRef, promptSpinnerColumns, promptValueRef, providerPrompt, queuedCompact, queuedVisible, renderedTranscriptItems, resizeEpoch, resizeState, restoreQueuedToPrompt, setPicker, setSlashIndex, setTextEntryLayoutRows, settingsPrompt, showWelcomeBanner, slashCommands, slashIndex, slashPaletteOpen, state, statuslineStats, store, toolApproval, toolOutputExpanded, transcriptContentHeight, transcriptGuardRows, transcriptMeasureRef, transcriptTailPinned, transcriptWindow, transientStatusWidth, tuiReady, usagePanel, viewportHeight, welcomePromptHintRows, welcomePromptHintText } = ctx; /* DESTRUCTURE */
|
|
24
24
|
const promptInputControl = (
|
|
25
25
|
<PromptInput
|
|
26
26
|
onSubmit={onSubmit}
|
|
@@ -47,6 +47,7 @@ export function renderAppView(ctx) {
|
|
|
47
47
|
// actually be accepted — otherwise Enter must submit the raw text as
|
|
48
48
|
// before instead of dead-ending in the palette accept path.
|
|
49
49
|
commandPaletteActive={slashPaletteOpen && slashCommands.length > 0}
|
|
50
|
+
commandPaletteOpen={slashPaletteOpen}
|
|
50
51
|
onCommandPaletteNavigate={(direction) => {
|
|
51
52
|
setSlashIndex((index) => {
|
|
52
53
|
const total = slashCommands.length;
|
|
@@ -65,7 +66,9 @@ export function renderAppView(ctx) {
|
|
|
65
66
|
onCommandPaletteAccept={acceptSlashPalette}
|
|
66
67
|
onCommandPaletteCancel={cancelSlashPalette}
|
|
67
68
|
onCommandPaletteComplete={completeSlashPalette}
|
|
68
|
-
onRestoreQueued={
|
|
69
|
+
onRestoreQueued={restoreQueuedToPrompt}
|
|
70
|
+
hasQueuedMessages={Array.isArray(state.queued) && state.queued.length > 0}
|
|
71
|
+
hasMessages={hasUserMessages}
|
|
69
72
|
/>
|
|
70
73
|
);
|
|
71
74
|
|
|
@@ -39,20 +39,20 @@ export function createCoreMemoryPicker({
|
|
|
39
39
|
});
|
|
40
40
|
void store.memoryControl?.({ action: 'core', op: 'list', project_id: '*' }, { silent: true })
|
|
41
41
|
.then(async (result) => {
|
|
42
|
-
//
|
|
42
|
+
// The recap toggle lives here too so /memory is a one-stop surface:
|
|
43
43
|
// toggle row + add row + curated entries.
|
|
44
|
-
const
|
|
45
|
-
const
|
|
44
|
+
const recap = (await store.getRecapSettings?.()) || { enabled: true };
|
|
45
|
+
const recapOn = recap.enabled !== false;
|
|
46
46
|
const coreRows = parseMemoryCoreRows(result);
|
|
47
47
|
const rows = [
|
|
48
48
|
{
|
|
49
|
-
value: '
|
|
50
|
-
label: '
|
|
51
|
-
meta:
|
|
52
|
-
description:
|
|
53
|
-
? 'Background
|
|
54
|
-
: 'Background memory
|
|
55
|
-
_action: 'toggle-
|
|
49
|
+
value: 'recap-toggle',
|
|
50
|
+
label: 'Recap',
|
|
51
|
+
meta: recapOn ? 'On' : 'Off',
|
|
52
|
+
description: recapOn
|
|
53
|
+
? 'Background session recaps on'
|
|
54
|
+
: 'Background recaps off; core memory remains available',
|
|
55
|
+
_action: 'toggle-recap',
|
|
56
56
|
},
|
|
57
57
|
{ value: 'core-add', label: 'Add Memory', description: 'store a new curated memory sentence', _action: 'add-core' },
|
|
58
58
|
{
|
|
@@ -76,17 +76,17 @@ export function createCoreMemoryPicker({
|
|
|
76
76
|
labelWidth: 12,
|
|
77
77
|
footer: (item) => (item && item._action === 'core-entry' ? (item._summary || item._element || '') : ''),
|
|
78
78
|
onSelect: (_value, item) => {
|
|
79
|
-
if (item?._action === 'toggle-
|
|
79
|
+
if (item?._action === 'toggle-recap') toggleRecapEnabled();
|
|
80
80
|
else if (item?._action === 'add-core') beginAddCoreMemory();
|
|
81
81
|
else if (item?._action === 'core-list') openCoreMemoryListPicker(item._rows);
|
|
82
82
|
else if (item?._line) store.pushNotice(item._line, 'info');
|
|
83
83
|
},
|
|
84
84
|
// Toggle rows also respond to ←/→ (matches settings/channels UX).
|
|
85
85
|
onLeft: (item) => {
|
|
86
|
-
if (item?._action === 'toggle-
|
|
86
|
+
if (item?._action === 'toggle-recap') toggleRecapEnabled();
|
|
87
87
|
},
|
|
88
88
|
onRight: (item) => {
|
|
89
|
-
if (item?._action === 'toggle-
|
|
89
|
+
if (item?._action === 'toggle-recap') toggleRecapEnabled();
|
|
90
90
|
},
|
|
91
91
|
onCancel: closeMemoryCorePicker,
|
|
92
92
|
});
|
|
@@ -137,14 +137,14 @@ export function createCoreMemoryPicker({
|
|
|
137
137
|
});
|
|
138
138
|
};
|
|
139
139
|
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
void Promise.resolve(store.
|
|
140
|
+
const toggleRecapEnabled = async () => {
|
|
141
|
+
const recap = (await store.getRecapSettings?.()) || { enabled: true };
|
|
142
|
+
void Promise.resolve(store.setRecapEnabled?.(!(recap.enabled !== false)))
|
|
143
143
|
.then((next) => {
|
|
144
|
-
if (!next) store.pushNotice('
|
|
145
|
-
else store.pushNotice(`
|
|
144
|
+
if (!next) store.pushNotice('recap setting is busy', 'warn');
|
|
145
|
+
else store.pushNotice(`Recap ${next.enabled ? 'on' : 'off'}`, 'info');
|
|
146
146
|
})
|
|
147
|
-
.catch((e) => store.pushNotice(`
|
|
147
|
+
.catch((e) => store.pushNotice(`recap toggle failed: ${e?.message || e}`, 'error'))
|
|
148
148
|
.finally(() => openMemoryCorePicker());
|
|
149
149
|
};
|
|
150
150
|
|