mixdog 0.9.19 → 0.9.21
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/LICENSE +21 -0
- package/README.md +100 -34
- package/package.json +3 -2
- package/scripts/build-tui.mjs +6 -0
- package/scripts/hook-bus-test.mjs +8 -0
- package/scripts/log-writer-guard-smoke.mjs +131 -0
- package/scripts/reactive-compact-persist-smoke.mjs +22 -6
- package/scripts/recall-bench-cases.json +0 -1
- package/scripts/recall-quality-cases.json +1 -2
- package/scripts/session-ingest-compaction-smoke.mjs +241 -0
- package/scripts/tool-smoke.mjs +150 -45
- package/src/defaults/skills/setup/SKILL.md +327 -0
- package/src/help.mjs +2 -5
- package/src/lib/mixdog-debug.cjs +13 -0
- package/src/mixdog-session-runtime.mjs +7 -3328
- package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
- package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
- package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
- package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
- package/src/runtime/channels/index.mjs +6 -2183
- package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
- package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
- package/src/runtime/channels/lib/network-retry.mjs +23 -0
- package/src/runtime/channels/lib/owned-runtime.mjs +627 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
- package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
- package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
- package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
- package/src/runtime/channels/lib/worker-main.mjs +777 -0
- package/src/runtime/memory/index.mjs +163 -1725
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
- package/src/runtime/memory/lib/http-router.mjs +811 -0
- package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
- package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
- package/src/runtime/memory/lib/memory-embed.mjs +28 -7
- package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
- package/src/runtime/memory/lib/memory.mjs +39 -0
- package/src/runtime/memory/lib/query-handlers.mjs +2 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
- package/src/runtime/shared/atomic-file.mjs +138 -80
- package/src/runtime/shared/child-guardian.mjs +61 -3
- package/src/session-runtime/boot-profile.mjs +36 -0
- package/src/session-runtime/channel-config-api.mjs +70 -0
- package/src/session-runtime/context-status.mjs +181 -0
- package/src/session-runtime/env.mjs +17 -0
- package/src/session-runtime/lifecycle-api.mjs +242 -0
- package/src/session-runtime/model-route-api.mjs +198 -0
- package/src/session-runtime/provider-auth-api.mjs +135 -0
- package/src/session-runtime/resource-api.mjs +282 -0
- package/src/session-runtime/runtime-core.mjs +2104 -0
- package/src/session-runtime/session-turn-api.mjs +274 -0
- package/src/session-runtime/tool-catalog.mjs +18 -264
- package/src/session-runtime/tool-defs.mjs +2 -2
- package/src/session-runtime/workflow-agents-api.mjs +238 -0
- package/src/standalone/agent-tool.mjs +2 -2
- package/src/standalone/channel-worker.mjs +67 -7
- package/src/standalone/folder-dialog.mjs +4 -1
- package/src/standalone/memory-runtime-proxy.mjs +154 -17
- package/src/standalone/seeds.mjs +28 -1
- package/src/tui/App.jsx +40 -28
- package/src/tui/app/core-memory-picker.mjs +1 -1
- package/src/tui/app/doctor.mjs +175 -0
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +9 -0
- package/src/tui/app/use-mouse-input.mjs +6 -0
- package/src/tui/app/use-transcript-scroll.mjs +77 -3
- package/src/tui/dist/index.mjs +2851 -2162
- package/src/tui/engine/context-state.mjs +145 -0
- package/src/tui/engine/prompt-history.mjs +27 -0
- package/src/tui/engine/session-api-ext.mjs +478 -0
- package/src/tui/engine/session-api.mjs +564 -0
- package/src/tui/engine/session-flow.mjs +485 -0
- package/src/tui/engine/turn.mjs +1078 -0
- package/src/tui/engine.mjs +68 -2620
- package/src/tui/index.jsx +7 -0
- package/vendor/ink/build/ink.js +16 -1
- package/vendor/ink/build/output.js +30 -4
- package/vendor/ink/build/render.js +5 -0
- package/src/workflows/sequential/WORKFLOW.md +0 -51
package/src/standalone/seeds.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { mkdirSync } from 'node:fs';
|
|
1
|
+
import { cpSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
2
3
|
|
|
3
4
|
export function ensureStandaloneEnvironment({ rootDir, dataDir }) {
|
|
4
5
|
if (!rootDir) throw new Error('standalone rootDir is required');
|
|
@@ -14,4 +15,30 @@ export function ensureStandaloneEnvironment({ rootDir, dataDir }) {
|
|
|
14
15
|
process.env.MIXDOG_PATCH_NATIVE_PREWARM ??= '0';
|
|
15
16
|
|
|
16
17
|
mkdirSync(dataDir, { recursive: true });
|
|
18
|
+
seedBundledSkills({ rootDir, dataDir });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Copy skills bundled in the package (src/defaults/skills/<name>/) into the
|
|
22
|
+
// user data skills dir, but only when the target dir does not already exist —
|
|
23
|
+
// never overwrite user-owned skill dirs.
|
|
24
|
+
export function seedBundledSkills({ rootDir, dataDir }) {
|
|
25
|
+
const bundledDir = join(rootDir, 'defaults', 'skills');
|
|
26
|
+
if (!existsSync(bundledDir)) return;
|
|
27
|
+
const targetRoot = join(dataDir, 'skills');
|
|
28
|
+
let names;
|
|
29
|
+
try {
|
|
30
|
+
names = readdirSync(bundledDir, { withFileTypes: true });
|
|
31
|
+
} catch {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
for (const entry of names) {
|
|
35
|
+
if (!entry.isDirectory()) continue;
|
|
36
|
+
const dest = join(targetRoot, entry.name);
|
|
37
|
+
if (existsSync(dest)) continue;
|
|
38
|
+
try {
|
|
39
|
+
cpSync(join(bundledDir, entry.name), dest, { recursive: true });
|
|
40
|
+
} catch {
|
|
41
|
+
// best-effort seeding; ignore individual copy failures
|
|
42
|
+
}
|
|
43
|
+
}
|
|
17
44
|
}
|
package/src/tui/App.jsx
CHANGED
|
@@ -720,6 +720,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
720
720
|
openContextPicker: (...a) => openContextPicker(...a),
|
|
721
721
|
openProfilePicker: (...a) => openProfilePicker(...a),
|
|
722
722
|
openUpdatePicker: (...a) => openUpdatePicker(...a),
|
|
723
|
+
runDoctor: (...a) => store.runDoctor?.(...a),
|
|
723
724
|
requestExit: (...a) => requestExit(...a),
|
|
724
725
|
});
|
|
725
726
|
const promptHintTimerRef = useRef(null);
|
|
@@ -845,34 +846,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
845
846
|
}, 2200);
|
|
846
847
|
}, []);
|
|
847
848
|
|
|
848
|
-
// Copy the currently-highlighted selection to the OS clipboard. ink's fork
|
|
849
|
-
// refreshed store.getRenderSelectionText() on the synchronous render that the
|
|
850
|
-
// final setSelection() triggered, so the selected text is ready to read.
|
|
851
|
-
const copySelection = useCallback((attempt = 0) => {
|
|
852
|
-
const renderText = store.getRenderSelectionText?.();
|
|
853
|
-
const remembered = selectionTextRef.current || '';
|
|
854
|
-
// A selection that has partially scrolled out of the viewport renders —
|
|
855
|
-
// and therefore harvests — only its visible rows. The remembered text
|
|
856
|
-
// (captured while the selection was last fully painted) is the fuller
|
|
857
|
-
// copy; prefer whichever is longer so scrolling never shrinks a copy.
|
|
858
|
-
const text = renderText == null
|
|
859
|
-
? remembered
|
|
860
|
-
: (remembered.length > renderText.length ? remembered : renderText);
|
|
861
|
-
if ((!text || !text.trim()) && attempt < 4) {
|
|
862
|
-
setTimeout(() => copySelection(attempt + 1), attempt === 0 ? 0 : 24);
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
if (!text || !text.trim()) return;
|
|
866
|
-
selectionTextRef.current = text;
|
|
867
|
-
copyToClipboard(text)
|
|
868
|
-
.then(() => {
|
|
869
|
-
const lines = text.split('\n').length;
|
|
870
|
-
const chars = text.length;
|
|
871
|
-
showSelectionCopyHint(`copied ${chars} char${chars === 1 ? '' : 's'}${lines > 1 ? ` · ${lines} lines` : ''}`, 'plain');
|
|
872
|
-
})
|
|
873
|
-
.catch((e) => showSelectionCopyHint(`copy failed: ${e?.message || e}`, 'error'));
|
|
874
|
-
}, [store, showSelectionCopyHint]);
|
|
875
|
-
|
|
876
849
|
// ── Post-mount input gate ──────────────────────────────────────────────
|
|
877
850
|
// Let one event-loop poll pass so Ink processes (and discards, because
|
|
878
851
|
// PromptInput is still disabled) any keystrokes queued during boot/first
|
|
@@ -1047,6 +1020,8 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1047
1020
|
scrollTranscriptRows,
|
|
1048
1021
|
queueScrollCoalesced,
|
|
1049
1022
|
moveSelectionFocus,
|
|
1023
|
+
getStitchedSelectionText,
|
|
1024
|
+
clearStitchBuffer,
|
|
1050
1025
|
} = useTranscriptScroll({
|
|
1051
1026
|
store,
|
|
1052
1027
|
frameColumns,
|
|
@@ -1067,6 +1042,42 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1067
1042
|
selectionTextRef,
|
|
1068
1043
|
});
|
|
1069
1044
|
|
|
1045
|
+
// Copy the currently-highlighted selection to the OS clipboard. ink's fork
|
|
1046
|
+
// refreshed store.getRenderSelectionText() on the synchronous render that the
|
|
1047
|
+
// final setSelection() triggered, so the selected text is ready to read.
|
|
1048
|
+
// NOTE: declared after useTranscriptScroll — the dependency array below
|
|
1049
|
+
// evaluates getStitchedSelectionText at render time, so referencing it
|
|
1050
|
+
// before the destructuring above is a TDZ ReferenceError.
|
|
1051
|
+
const copySelection = useCallback((attempt = 0) => {
|
|
1052
|
+
const renderText = store.getRenderSelectionText?.();
|
|
1053
|
+
const remembered = selectionTextRef.current || '';
|
|
1054
|
+
// A selection that has partially scrolled out of the viewport renders —
|
|
1055
|
+
// and therefore harvests — only its visible rows. The remembered text
|
|
1056
|
+
// (captured while the selection was last fully painted) is the fuller
|
|
1057
|
+
// copy; prefer whichever is longer so scrolling never shrinks a copy.
|
|
1058
|
+
let text = renderText == null
|
|
1059
|
+
? remembered
|
|
1060
|
+
: (remembered.length > renderText.length ? remembered : renderText);
|
|
1061
|
+
// The stitch buffer accumulates rows harvested across every scroll position
|
|
1062
|
+
// during a transcript drag, so it can reconstruct rows that scrolled out of
|
|
1063
|
+
// view entirely (neither renderText nor the last-full-paint remembered text
|
|
1064
|
+
// ever saw them). Prefer it only when it is strictly longer than both.
|
|
1065
|
+
const stitched = getStitchedSelectionText?.() || '';
|
|
1066
|
+
if (stitched.length > text.length) text = stitched;
|
|
1067
|
+
if ((!text || !text.trim()) && attempt < 4) {
|
|
1068
|
+
setTimeout(() => copySelection(attempt + 1), attempt === 0 ? 0 : 24);
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if (!text || !text.trim()) return;
|
|
1072
|
+
selectionTextRef.current = text;
|
|
1073
|
+
copyToClipboard(text)
|
|
1074
|
+
.then(() => {
|
|
1075
|
+
const lines = text.split('\n').length;
|
|
1076
|
+
const chars = text.length;
|
|
1077
|
+
showSelectionCopyHint(`copied ${chars} char${chars === 1 ? '' : 's'}${lines > 1 ? ` · ${lines} lines` : ''}`, 'plain');
|
|
1078
|
+
})
|
|
1079
|
+
.catch((e) => showSelectionCopyHint(`copy failed: ${e?.message || e}`, 'error'));
|
|
1080
|
+
}, [store, showSelectionCopyHint, getStitchedSelectionText]);
|
|
1070
1081
|
|
|
1071
1082
|
useEffect(() => () => {
|
|
1072
1083
|
stopSmoothScroll();
|
|
@@ -1102,6 +1113,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
1102
1113
|
queueScrollCoalesced,
|
|
1103
1114
|
setSlashIndex,
|
|
1104
1115
|
setMeasuredRowsVersion,
|
|
1116
|
+
clearStitchBuffer,
|
|
1105
1117
|
});
|
|
1106
1118
|
|
|
1107
1119
|
// Enable extended keyboard reporting (kitty + xterm modifyOtherKeys)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* doctor.mjs — /doctor installation health report builder.
|
|
3
|
+
*
|
|
4
|
+
* buildDoctorReport(runtime, getState) runs a fixed set of best-effort
|
|
5
|
+
* diagnostics against the live runtime accessors and returns a single
|
|
6
|
+
* multi-line string (one glyph-prefixed row per check). Every check is
|
|
7
|
+
* individually try/caught so one failure degrades to a single FAIL row
|
|
8
|
+
* instead of killing the whole report. No secrets are ever printed — only
|
|
9
|
+
* whether a token/auth is configured. The only network touch is the
|
|
10
|
+
* best-effort npm update check already used by /update; an unreachable
|
|
11
|
+
* registry downgrades to a WARN "check skipped" row, never a throw.
|
|
12
|
+
*/
|
|
13
|
+
import { compareSemver } from '../../runtime/shared/update-checker.mjs';
|
|
14
|
+
import { readFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const GLYPH = { ok: '✓', warn: '⚠', fail: '✗' };
|
|
19
|
+
|
|
20
|
+
function readPackageJson() {
|
|
21
|
+
try {
|
|
22
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const raw = JSON.parse(readFileSync(join(dir, '..', '..', '..', 'package.json'), 'utf8'));
|
|
24
|
+
return raw && typeof raw === 'object' ? raw : null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function buildDoctorReport(runtime = {}, getState = () => ({})) {
|
|
31
|
+
const rows = [];
|
|
32
|
+
const check = async (label, fn) => {
|
|
33
|
+
const row = (level, detail) => {
|
|
34
|
+
rows.push(`${GLYPH[level] || GLYPH.warn} ${label}: ${detail}`);
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
await fn(row);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
row('fail', `check failed: ${e?.message || e}`);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const pkg = readPackageJson();
|
|
43
|
+
|
|
44
|
+
// 1. mixdog version + update availability (best-effort registry check).
|
|
45
|
+
await check('mixdog', async (row) => {
|
|
46
|
+
const upd = (await runtime.checkForUpdate?.({})) || {};
|
|
47
|
+
const current = upd.currentVersion || pkg?.version || 'unknown';
|
|
48
|
+
const latest = upd.latestVersion;
|
|
49
|
+
if (latest == null) {
|
|
50
|
+
row('warn', `v${current} · update check skipped (registry unreachable)`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (upd.updateAvailable) row('warn', `v${current} · update available → v${latest}`);
|
|
54
|
+
else row('ok', `v${current} · up to date`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// 2. node version vs package.json engines (only when engines present).
|
|
58
|
+
await check('node', async (row) => {
|
|
59
|
+
const nodeVer = process.versions?.node || '0.0.0';
|
|
60
|
+
const engines = pkg?.engines?.node;
|
|
61
|
+
if (!engines) {
|
|
62
|
+
row('ok', `v${nodeVer}`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const m = String(engines).match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
|
|
66
|
+
if (!m) {
|
|
67
|
+
row('warn', `v${nodeVer} · engines "${engines}" unparsed`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const required = `${m[1]}.${m[2] || 0}.${m[3] || 0}`;
|
|
71
|
+
const ok = compareSemver(nodeVer, required) >= 0;
|
|
72
|
+
row(ok ? 'ok' : 'fail', `v${nodeVer} · requires node ${engines}`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// 3. providers: configured auth count; FAIL if the active route provider
|
|
76
|
+
// has no auth.
|
|
77
|
+
await check('providers', async (row) => {
|
|
78
|
+
const setup = (await runtime.getProviderSetup?.()) || {};
|
|
79
|
+
const lists = [...(setup.api || []), ...(setup.oauth || []), ...(setup.local || [])];
|
|
80
|
+
const isAuthed = (p) => Boolean(p && (p.authenticated || p.enabled || p.detected));
|
|
81
|
+
const authed = lists.filter(isAuthed);
|
|
82
|
+
const active = getState()?.provider || '';
|
|
83
|
+
const activeEntry = active ? lists.find((p) => p.id === active) : null;
|
|
84
|
+
if (activeEntry && !isAuthed(activeEntry)) {
|
|
85
|
+
row('fail', `route ${active} has no auth · ${authed.length} configured`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (active && !activeEntry) {
|
|
89
|
+
row('warn', `${authed.length} authed · route ${active} (not listed)`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
row('ok', `${authed.length} authed · route ${active || 'unknown'}`);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// 4. MCP: connected/configured, failed servers named.
|
|
96
|
+
await check('mcp', async (row) => {
|
|
97
|
+
const m = runtime.mcpStatus?.() || {};
|
|
98
|
+
const servers = Array.isArray(m.servers) ? m.servers : [];
|
|
99
|
+
const conn = Number(m.connectedCount || 0);
|
|
100
|
+
const conf = Number(m.configuredCount || 0);
|
|
101
|
+
if (conf === 0) {
|
|
102
|
+
row('ok', 'no servers configured');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const failed = servers
|
|
106
|
+
.filter((s) => s && (s.error || s.status === 'failed'))
|
|
107
|
+
.map((s) => s.name || s.id)
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
const detail = `${conn}/${conf} connected${failed.length ? ` · failed: ${failed.join(', ')}` : ''}`;
|
|
110
|
+
row(failed.length || conn < conf ? 'warn' : 'ok', detail);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// 5. memory: enabled/disabled (+ backend health only if the accessor
|
|
114
|
+
// already exposes it; no new probing).
|
|
115
|
+
await check('memory', async (row) => {
|
|
116
|
+
const mem = runtime.getMemorySettings?.() || {};
|
|
117
|
+
const enabled = mem.enabled !== false;
|
|
118
|
+
let detail = enabled ? 'enabled' : 'disabled';
|
|
119
|
+
if (mem.backend) detail += ` · backend ${mem.backend}`;
|
|
120
|
+
const health = mem.backendHealth || mem.health;
|
|
121
|
+
if (health != null) {
|
|
122
|
+
detail += ` · ${typeof health === 'string' ? health : (health.ok ? 'healthy' : 'unhealthy')}`;
|
|
123
|
+
}
|
|
124
|
+
row(enabled ? 'ok' : 'warn', detail);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// 6. channels: enabled + worker status + configured tokens (names only).
|
|
128
|
+
await check('channels', async (row) => {
|
|
129
|
+
const settings = runtime.getChannelSettings?.({ includeStatus: true }) || {};
|
|
130
|
+
const enabled = settings.enabled !== false;
|
|
131
|
+
if (!enabled) {
|
|
132
|
+
row('ok', 'disabled');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const worker = settings.status || runtime.getChannelWorkerStatus?.() || {};
|
|
136
|
+
const setup = runtime.getChannelSetup?.() || {};
|
|
137
|
+
const tokens = [];
|
|
138
|
+
if (setup.discord?.authenticated) tokens.push('discord');
|
|
139
|
+
if (setup.telegram?.authenticated) tokens.push('telegram');
|
|
140
|
+
if (setup.webhook?.authenticated) tokens.push('webhook');
|
|
141
|
+
const running = worker.running === true;
|
|
142
|
+
const detail = `enabled · worker ${running ? 'running' : 'stopped'} · tokens: ${tokens.length ? tokens.join(', ') : 'none'}`;
|
|
143
|
+
row(running ? 'ok' : 'warn', detail);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 7. skills / plugins / hooks: counts + broken/disabled entries.
|
|
147
|
+
await check('skills', async (row) => {
|
|
148
|
+
const s = runtime.skillsStatus?.() || {};
|
|
149
|
+
const skills = Array.isArray(s.skills) ? s.skills : [];
|
|
150
|
+
const broken = skills.filter((x) => x && (x.broken || x.error || x.invalid));
|
|
151
|
+
const disabled = skills.filter((x) => x && x.disabled);
|
|
152
|
+
let detail = `${s.count ?? skills.length} available`;
|
|
153
|
+
if (disabled.length) detail += ` · ${disabled.length} disabled`;
|
|
154
|
+
if (broken.length) detail += ` · broken: ${broken.map((x) => x.name || x.id).filter(Boolean).join(', ')}`;
|
|
155
|
+
row(broken.length ? 'warn' : 'ok', detail);
|
|
156
|
+
});
|
|
157
|
+
await check('plugins', async (row) => {
|
|
158
|
+
const p = runtime.pluginsStatus?.() || {};
|
|
159
|
+
const plugins = Array.isArray(p.plugins) ? p.plugins : [];
|
|
160
|
+
const broken = plugins.filter((x) => x && (x.broken || x.error));
|
|
161
|
+
const disabled = plugins.filter((x) => x && x.disabled);
|
|
162
|
+
let detail = `${p.count ?? plugins.length} detected`;
|
|
163
|
+
if (disabled.length) detail += ` · ${disabled.length} disabled`;
|
|
164
|
+
if (broken.length) detail += ` · broken: ${broken.map((x) => x.title || x.name || x.id).filter(Boolean).join(', ')}`;
|
|
165
|
+
row(broken.length ? 'warn' : 'ok', detail);
|
|
166
|
+
});
|
|
167
|
+
await check('hooks', async (row) => {
|
|
168
|
+
const h = runtime.hooksStatus?.() || {};
|
|
169
|
+
const events = Array.isArray(h.events) ? h.events : [];
|
|
170
|
+
const enabled = h.enabled === true;
|
|
171
|
+
row('ok', `${enabled ? 'enabled' : 'disabled'} · ${events.length} event${events.length === 1 ? '' : 's'}`);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return ['mixdog doctor — installation health', ...rows].join('\n');
|
|
175
|
+
}
|
|
@@ -32,6 +32,7 @@ export const SLASH_COMMANDS = [
|
|
|
32
32
|
{ name: 'settings', usage: '/setting', aliases: ['setting', 'config'], aliasUsage: ['settings', 'config'], showAliasUsage: false, description: 'Open runtime settings' },
|
|
33
33
|
{ name: 'profile', usage: '/profile', description: 'Set your title and response language' },
|
|
34
34
|
{ name: 'update', usage: '/update', description: 'Check version and update mixdog' },
|
|
35
|
+
{ name: 'doctor', usage: '/doctor', description: 'Diagnose installation health' },
|
|
35
36
|
{ name: 'quit', usage: '/quit', aliases: ['exit', 'q'], aliasUsage: ['exit', 'q'], description: 'Quit the TUI' },
|
|
36
37
|
];
|
|
37
38
|
|
|
@@ -46,6 +46,7 @@ export function createSlashDispatch({
|
|
|
46
46
|
openContextPicker,
|
|
47
47
|
openProfilePicker,
|
|
48
48
|
openUpdatePicker,
|
|
49
|
+
runDoctor,
|
|
49
50
|
requestExit,
|
|
50
51
|
}) {
|
|
51
52
|
const runSlashCommand = (cmd, arg = '') => {
|
|
@@ -383,6 +384,14 @@ export function createSlashDispatch({
|
|
|
383
384
|
case 'update':
|
|
384
385
|
openUpdatePicker();
|
|
385
386
|
return true;
|
|
387
|
+
case 'doctor':
|
|
388
|
+
if (state.commandBusy) {
|
|
389
|
+
store.pushNotice('wait for the current command to finish before /doctor', 'warn');
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
void Promise.resolve(runDoctor?.())
|
|
393
|
+
.catch((e) => store.pushNotice(`doctor failed: ${e?.message || e}`, 'error'));
|
|
394
|
+
return true;
|
|
386
395
|
case 'quit':
|
|
387
396
|
requestExit();
|
|
388
397
|
return true;
|
|
@@ -54,6 +54,7 @@ export function useMouseInput({
|
|
|
54
54
|
queueScrollCoalesced,
|
|
55
55
|
setSlashIndex,
|
|
56
56
|
setMeasuredRowsVersion,
|
|
57
|
+
clearStitchBuffer,
|
|
57
58
|
}) {
|
|
58
59
|
const mouseZoomPassthroughTimerRef = useRef(null);
|
|
59
60
|
|
|
@@ -353,6 +354,8 @@ export function useMouseInput({
|
|
|
353
354
|
const hi = { x: wr.x2, y: wr.y2 };
|
|
354
355
|
const rect = linearSelection(lo, hi);
|
|
355
356
|
stopSmoothScroll();
|
|
357
|
+
// Fresh word/line anchor: reset the stitch buffer (see char-drag).
|
|
358
|
+
clearStitchBuffer?.();
|
|
356
359
|
dragRef.current = {
|
|
357
360
|
anchor: { x, y },
|
|
358
361
|
anchorScroll: region === 'transcript' ? scrollTargetRef.current : 0,
|
|
@@ -376,6 +379,9 @@ export function useMouseInput({
|
|
|
376
379
|
// there; keep the transcript scroll anchor only for the transcript.
|
|
377
380
|
// Plain single press clears any word/line anchorSpan (char-drag mode).
|
|
378
381
|
stopSmoothScroll();
|
|
382
|
+
// Fresh char-drag anchor: drop any rows stitched from a prior
|
|
383
|
+
// selection so the new drag reconstructs only its own content.
|
|
384
|
+
clearStitchBuffer?.();
|
|
379
385
|
dragRef.current = {
|
|
380
386
|
anchor: { x, y },
|
|
381
387
|
anchorScroll: region === 'transcript' ? scrollTargetRef.current : 0,
|
|
@@ -44,6 +44,71 @@ export function useTranscriptScroll({
|
|
|
44
44
|
// SCROLL_COALESCE_MS). Both call sites accumulate into pendingRows.
|
|
45
45
|
const scrollCoalesceRef = useRef({ pendingRows: 0, timer: null });
|
|
46
46
|
const selectionTextTimerRef = useRef(null);
|
|
47
|
+
// Stitch buffer: accumulates harvested transcript selection rows across scroll
|
|
48
|
+
// positions so Ctrl+C copies the FULL drag even after it auto-scrolled past the
|
|
49
|
+
// viewport. Keyed by scroll-invariant content row = screenY - scrollTarget at
|
|
50
|
+
// harvest time; value = row text. Only transcript-region drags accumulate.
|
|
51
|
+
const stitchBufferRef = useRef(new Map());
|
|
52
|
+
const stitchHarvestTimerRef = useRef(null);
|
|
53
|
+
// Scroll offset captured at the SCHEDULE (paint) time of the pending harvest —
|
|
54
|
+
// the deferred timer must key rows by the frame's scroll, not by whatever
|
|
55
|
+
// scrollTargetRef holds when the timer eventually fires (a scroll in between
|
|
56
|
+
// would mis-key the rows). Latest schedule wins (latest paint = latest frame).
|
|
57
|
+
const stitchHarvestScrollRef = useRef(0);
|
|
58
|
+
|
|
59
|
+
const clearStitchBuffer = useCallback(() => {
|
|
60
|
+
stitchBufferRef.current.clear();
|
|
61
|
+
if (stitchHarvestTimerRef.current) {
|
|
62
|
+
clearTimeout(stitchHarvestTimerRef.current);
|
|
63
|
+
stitchHarvestTimerRef.current = null;
|
|
64
|
+
}
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
// Deferred (like rememberSelectionTextSoon) harvest of the currently visible
|
|
68
|
+
// selection rows into the stitch buffer. Runs on EVERY transcript selection
|
|
69
|
+
// paint AND on scroll-shift repaints (the rememberText:false path) so rows
|
|
70
|
+
// revealed only mid-scroll are captured. Later harvest of a key overwrites,
|
|
71
|
+
// handling partial↔full endpoint rows on retraction.
|
|
72
|
+
const harvestStitchRowsSoon = useCallback(() => {
|
|
73
|
+
if (dragRef.current.region !== 'transcript') return;
|
|
74
|
+
// Capture the scroll offset for THIS paint (schedule time). If a timer is
|
|
75
|
+
// already pending, only refresh the captured offset to the latest frame and
|
|
76
|
+
// reuse the existing timer.
|
|
77
|
+
stitchHarvestScrollRef.current = Number(scrollTargetRef.current) || 0;
|
|
78
|
+
if (stitchHarvestTimerRef.current) return;
|
|
79
|
+
stitchHarvestTimerRef.current = setTimeout(() => {
|
|
80
|
+
stitchHarvestTimerRef.current = null;
|
|
81
|
+
if (dragRef.current.region !== 'transcript') return;
|
|
82
|
+
const rows = store.getRenderSelectionRows?.();
|
|
83
|
+
if (!Array.isArray(rows)) return;
|
|
84
|
+
const scroll = stitchHarvestScrollRef.current;
|
|
85
|
+
for (const row of rows) {
|
|
86
|
+
if (!row || typeof row.y !== 'number') continue;
|
|
87
|
+
stitchBufferRef.current.set(row.y - scroll, typeof row.text === 'string' ? row.text : '');
|
|
88
|
+
}
|
|
89
|
+
}, 0);
|
|
90
|
+
}, [store]);
|
|
91
|
+
|
|
92
|
+
// Map the CURRENT rect + current scrollTarget onto the content-key range and
|
|
93
|
+
// join buffered rows sorted by key with '\n' (skipping missing keys). Returns
|
|
94
|
+
// '' when unusable so callers can fall back to render/remembered text.
|
|
95
|
+
const getStitchedSelectionText = useCallback(() => {
|
|
96
|
+
const buf = stitchBufferRef.current;
|
|
97
|
+
if (!buf.size) return '';
|
|
98
|
+
if (dragRef.current.region !== 'transcript') return '';
|
|
99
|
+
const rect = dragRef.current.rect;
|
|
100
|
+
if (!rect) return '';
|
|
101
|
+
const y1 = Number(rect.y1);
|
|
102
|
+
const y2 = Number(rect.y2);
|
|
103
|
+
if (!Number.isFinite(y1) || !Number.isFinite(y2)) return '';
|
|
104
|
+
const scroll = Number(scrollTargetRef.current) || 0;
|
|
105
|
+
const lo = Math.min(y1, y2) - scroll;
|
|
106
|
+
const hi = Math.max(y1, y2) - scroll;
|
|
107
|
+
const keys = [...buf.keys()].filter((k) => k >= lo && k <= hi).sort((a, b) => a - b);
|
|
108
|
+
if (!keys.length) return '';
|
|
109
|
+
const text = keys.map((k) => buf.get(k)).filter((t) => t != null).join('\n');
|
|
110
|
+
return text.trim() ? text : '';
|
|
111
|
+
}, []);
|
|
47
112
|
|
|
48
113
|
const stopSmoothScroll = useCallback(() => {
|
|
49
114
|
if (!scrollAnimationRef.current) return;
|
|
@@ -145,19 +210,24 @@ export function useTranscriptScroll({
|
|
|
145
210
|
store.setRenderSelection?.(nextRect, { immediate: true });
|
|
146
211
|
}
|
|
147
212
|
if (needsCapture) rememberSelectionTextSoon();
|
|
213
|
+
if (nextRect) harvestStitchRowsSoon();
|
|
148
214
|
return true;
|
|
149
215
|
}
|
|
150
216
|
state.rect = nextRect;
|
|
151
217
|
state.t = Date.now();
|
|
152
218
|
store.setRenderSelection?.(nextRect, immediate ? { immediate: true } : undefined);
|
|
153
219
|
if (nextRect && rememberText && nextRect.captureText !== false) rememberSelectionTextSoon();
|
|
220
|
+
if (nextRect) harvestStitchRowsSoon();
|
|
154
221
|
return true;
|
|
155
|
-
}, [store, rememberSelectionTextSoon]);
|
|
222
|
+
}, [store, rememberSelectionTextSoon, harvestStitchRowsSoon]);
|
|
156
223
|
|
|
157
224
|
const applySelectionRect = useCallback((rect) => {
|
|
158
225
|
const clippedRect = withSelectionClip(rect);
|
|
159
226
|
dragRef.current.rect = clippedRect || null;
|
|
160
|
-
if (!clippedRect)
|
|
227
|
+
if (!clippedRect) {
|
|
228
|
+
selectionTextRef.current = '';
|
|
229
|
+
clearStitchBuffer();
|
|
230
|
+
}
|
|
161
231
|
const state = selectionPaintRef.current;
|
|
162
232
|
if (state.timer) {
|
|
163
233
|
clearTimeout(state.timer);
|
|
@@ -165,7 +235,7 @@ export function useTranscriptScroll({
|
|
|
165
235
|
state.pending = null;
|
|
166
236
|
}
|
|
167
237
|
paintSelectionRect(clippedRect, { rememberText: true, immediate: true });
|
|
168
|
-
}, [paintSelectionRect, withSelectionClip]);
|
|
238
|
+
}, [paintSelectionRect, withSelectionClip, clearStitchBuffer]);
|
|
169
239
|
|
|
170
240
|
const applySelectionRectThrottled = useCallback((rect) => {
|
|
171
241
|
const clippedRect = withSelectionClip(rect, { captureText: false });
|
|
@@ -274,6 +344,8 @@ export function useTranscriptScroll({
|
|
|
274
344
|
paintState.pending = null;
|
|
275
345
|
if (selectionTextTimerRef.current) clearTimeout(selectionTextTimerRef.current);
|
|
276
346
|
selectionTextTimerRef.current = null;
|
|
347
|
+
if (stitchHarvestTimerRef.current) clearTimeout(stitchHarvestTimerRef.current);
|
|
348
|
+
stitchHarvestTimerRef.current = null;
|
|
277
349
|
const coalesceState = scrollCoalesceRef.current;
|
|
278
350
|
if (coalesceState.timer) clearTimeout(coalesceState.timer);
|
|
279
351
|
coalesceState.timer = null;
|
|
@@ -512,5 +584,7 @@ export function useTranscriptScroll({
|
|
|
512
584
|
scrollTranscriptRows,
|
|
513
585
|
queueScrollCoalesced,
|
|
514
586
|
moveSelectionFocus,
|
|
587
|
+
getStitchedSelectionText,
|
|
588
|
+
clearStitchBuffer,
|
|
515
589
|
};
|
|
516
590
|
}
|