aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAgando <roduccomm@gmail.com>2026-07-02 06:00:40 +0200
committerAgando <roduccomm@gmail.com>2026-07-02 06:00:40 +0200
commitea4d09fb5ccba12ed774fcf7c913edfa1e6a851a (patch)
tree2361e33e52cdb4e0880ed0c776ed75943b37d7cf
parent8b3e3e4527772bf060984f98777a2cff8e88aa09 (diff)
auto: 2026-07-02_06:00 changes
-rw-r--r--cheatsheet.html57
-rw-r--r--downloads/OrbitControls.js1045
-rw-r--r--help.html8
-rw-r--r--main.html6
-rw-r--r--modules-index.json4
-rw-r--r--modules/core/core-bundle.js552
-rw-r--r--modules/core/module-aiassist.js333
-rw-r--r--modules/core/module-loader.js4
-rw-r--r--modules/core/module-manager.js5
-rw-r--r--modules/core/module-modmgr.js29
-rw-r--r--modules/core/module-smartctx.js42
-rw-r--r--modules/ext/mindmap.js65
-rw-r--r--modules/ext/three.js1176
-rw-r--r--packages.json10
-rw-r--r--review-newos4.html32
-rw-r--r--v4.5.8043
16 files changed, 3294 insertions, 117 deletions
diff --git a/cheatsheet.html b/cheatsheet.html
index d85b17a..5bbcd24 100644
--- a/cheatsheet.html
+++ b/cheatsheet.html
@@ -75,6 +75,12 @@ pre{background:var(--bg2);border:1px solid var(--border);border-radius:var(--rad
<div class="item"><span class="cmd">bench-compare &lt;p1&gt; &lt;p2&gt;</span><span class="desc">Compare benchmark results</span></div>
<div class="item"><span class="cmd">mem-debug</span><span class="desc">Diagnose memory (heap, logs, caps)</span></div>
<div class="item"><span class="cmd">reset-mem</span><span class="desc">Clear AI internal logs</span></div>
+<div class="item"><span class="cmd">debug-ai</span><span class="desc">Toggle tool-call debug panel</span></div>
+<div class="item"><span class="cmd">debug-ai-last</span><span class="desc">Show last AI response</span></div>
+<div class="item"><span class="cmd">debug-ai-send</span><span class="desc">Show what was sent to AI</span></div>
+<div class="item"><span class="cmd">debug-ai-log</span><span class="desc">Dump full tool call log</span></div>
+<div class="item"><span class="cmd">archive-list</span><span class="desc">List archived sessions</span></div>
+<div class="item"><span class="cmd">archive-read &lt;file&gt;</span><span class="desc">View an archive</span></div>
</div>
<h2>🧩 Modules</h2>
@@ -122,27 +128,28 @@ pre{background:var(--bg2);border:1px solid var(--border);border-radius:var(--rad
<div class="item"><span class="cmd">cron-list / cron-stop</span><span class="desc">Manage jobs</span></div>
<div class="item"><span class="cmd">wpm</span><span class="desc">Package manager</span></div>
<div class="item"><span class="cmd">mindmap</span><span class="desc">Brainstorming tool</span></div>
- <div class="item"><span class="cmd">webdav-status</span><span class="desc">Cloud sync status</span></div>
- <div class="item"><span class="cmd">webdav-push / pull</span><span class="desc">Sync files</span></div>
- <div class="item"><span class="cmd">webdav-browser</span><span class="desc">File-tree UI browser</span></div>
- <div class="item"><span class="cmd">webdav-rm / mkdir</span><span class="desc">Delete / create dir</span></div>
- <div class="item"><span class="cmd">webdav-rename / move</span><span class="desc">Rename / move files</span></div>
- <div class="item"><span class="cmd">webdav-backup</span><span class="desc">Backup space</span></div>
- <div class="item"><span class="cmd">theme &lt;name&gt;</span><span class="desc">Change theme</span></div>
- <div class="item"><span class="cmd">voice</span><span class="desc">Speech-to-text</span></div>
- <div class="item"><span class="cmd">chat</span><span class="desc">Chat overlay</span></div>
- <div class="item"><span class="cmd">gzip / gunzip</span><span class="desc">Compress files</span></div>
- <div class="item"><span class="cmd">encrypt / decrypt</span><span class="desc">AES-256 encrypt</span></div>
- <div class="item"><span class="cmd">floating-add</span><span class="desc">Add floating btn/menu/panel</span></div>
- <div class="item"><span class="cmd">floating-remove &lt;id&gt;</span><span class="desc">Remove floating element</span></div>
- <div class="item"><span class="cmd">floating-list</span><span class="desc">List floating elements</span></div>
- <div class="item"><span class="cmd">floating-pos &lt;id&gt; x y</span><span class="desc">Reposition element</span></div>
- <div class="item"><span class="cmd">floating-toggle &lt;id&gt;</span><span class="desc">Show/hide element</span></div>
- <div class="item"><span class="cmd">debug</span><span class="desc">Toggle debug panel</span></div>
- <div class="item"><span class="cmd">agent-list</span><span class="desc">List registered agents</span></div>
- <div class="item"><span class="cmd">lease-list</span><span class="desc">Show active leases</span></div>
- <div class="item"><span class="cmd">orchestrate</span><span class="desc">Multi-step task agent</span></div>
- <div class="item"><span class="cmd">predict &lt;task&gt;</span><span class="desc">Predict task duration/turns</span></div>
+<div class="item"><span class="cmd">webdav-status</span><span class="desc">Cloud sync status</span></div>
+<div class="item"><span class="cmd">webdav-push / pull</span><span class="desc">Sync files</span></div>
+<div class="item"><span class="cmd">webdav-browser</span><span class="desc">File-tree UI browser</span></div>
+<div class="item"><span class="cmd">webdav-rm / mkdir</span><span class="desc">Delete / create dir</span></div>
+<div class="item"><span class="cmd">webdav-rename / move</span><span class="desc">Rename / move files</span></div>
+<div class="item"><span class="cmd">webdav-backup</span><span class="desc">Backup space</span></div>
+<div class="item"><span class="cmd">theme &lt;name&gt;</span><span class="desc">Change theme</span></div>
+<div class="item"><span class="cmd">voice</span><span class="desc">Speech-to-text</span></div>
+<div class="item"><span class="cmd">chat</span><span class="desc">Chat overlay</span></div>
+<div class="item"><span class="cmd">gzip / gunzip</span><span class="desc">Compress files</span></div>
+<div class="item"><span class="cmd">encrypt / decrypt</span><span class="desc">AES-256 encrypt</span></div>
+<div class="item"><span class="cmd">floating-add</span><span class="desc">Add floating btn/menu/panel</span></div>
+<div class="item"><span class="cmd">floating-remove &lt;id&gt;</span><span class="desc">Remove floating element</span></div>
+<div class="item"><span class="cmd">floating-list</span><span class="desc">List floating elements</span></div>
+<div class="item"><span class="cmd">floating-pos &lt;id&gt; x y</span><span class="desc">Reposition element</span></div>
+<div class="item"><span class="cmd">floating-toggle &lt;id&gt;</span><span class="desc">Show/hide element</span></div>
+<div class="item"><span class="cmd">three</span><span class="desc">3D scene builder (Three.js)</span></div>
+<div class="item"><span class="cmd">debug</span><span class="desc">Toggle debug panel</span></div>
+<div class="item"><span class="cmd">agent-list</span><span class="desc">List registered agents</span></div>
+<div class="item"><span class="cmd">lease-list</span><span class="desc">Show active leases</span></div>
+<div class="item"><span class="cmd">orchestrate</span><span class="desc">Multi-step task agent</span></div>
+<div class="item"><span class="cmd">predict &lt;task&gt;</span><span class="desc">Predict task duration/turns</span></div>
<div class="item"><span class="cmd">predictor-status</span><span class="desc">Model accuracy & data</span></div>
<div class="item"><span class="cmd">sandbox-status</span><span class="desc">Sandbox pool health</span></div>
<div class="item"><span class="cmd">sandbox-run &lt;t&gt;</span><span class="desc">Run in sandbox</span></div>
@@ -151,15 +158,15 @@ pre{background:var(--bg2);border:1px solid var(--border);border-radius:var(--rad
<div class="item"><span class="cmd">memgraph-search</span><span class="desc">Search memory graph</span></div>
<div class="item"><span class="cmd">memgraph-stats</span><span class="desc">Graph analysis</span></div>
<div class="item"><span class="cmd">memgraph-prune</span><span class="desc">Prune stale edges</span></div>
- <div class="item"><span class="cmd">memgraph-learn</span><span class="desc">Add fact to graph</span></div>
+<div class="item"><span class="cmd">memgraph-learn</span><span class="desc">Add fact to graph</span></div>
<div class="item"><span class="cmd">world-model</span><span class="desc">Full world model</span></div>
<div class="item"><span class="cmd">world-model-summary</span><span class="desc">System self-awareness</span></div>
<div class="item"><span class="cmd">world-model-ask</span><span class="desc">Query the model</span></div>
<div class="item"><span class="cmd">world-model-refresh</span><span class="desc">Re-synthesize</span></div>
<div class="item"><span class="cmd">security-level &lt;0-3&gt;</span><span class="desc">Set security level</span></div>
- <div class="item"><span class="cmd">cmd-allow / cmd-deny</span><span class="desc">Per-command access</span></div>
- <div class="item"><span class="cmd">checkpoint</span><span class="desc">Snapshot VFS state</span></div>
- <div class="item"><span class="cmd">health-check</span><span class="desc">System health check</span></div>
+<div class="item"><span class="cmd">cmd-allow / cmd-deny</span><span class="desc">Per-command access</span></div>
+<div class="item"><span class="cmd">checkpoint</span><span class="desc">Snapshot VFS state</span></div>
+<div class="item"><span class="cmd">health-check</span><span class="desc">System health check</span></div>
</div>
<h2>🐚 Shell</h2>
diff --git a/downloads/OrbitControls.js b/downloads/OrbitControls.js
new file mode 100644
index 0000000..0873097
--- /dev/null
+++ b/downloads/OrbitControls.js
@@ -0,0 +1,1045 @@
+( function () {
+
+ // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
+ //
+ // Orbit - left mouse / touch: one-finger move
+ // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
+ // Pan - right mouse, or left mouse + ctrl/meta/shiftKey, or arrow keys / touch: two-finger move
+
+ const _changeEvent = {
+ type: 'change'
+ };
+ const _startEvent = {
+ type: 'start'
+ };
+ const _endEvent = {
+ type: 'end'
+ };
+
+ class OrbitControls extends THREE.EventDispatcher {
+
+ constructor( object, domElement ) {
+
+ super();
+ if ( domElement === undefined ) console.warn( 'THREE.OrbitControls: The second parameter "domElement" is now mandatory.' );
+ if ( domElement === document ) console.error( 'THREE.OrbitControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' );
+ this.object = object;
+ this.domElement = domElement; // Set to false to disable this control
+
+ this.enabled = true; // "target" sets the location of focus, where the object orbits around
+
+ this.target = new THREE.Vector3(); // How far you can dolly in and out ( PerspectiveCamera only )
+
+ this.minDistance = 0;
+ this.maxDistance = Infinity; // How far you can zoom in and out ( OrthographicCamera only )
+
+ this.minZoom = 0;
+ this.maxZoom = Infinity; // How far you can orbit vertically, upper and lower limits.
+ // Range is 0 to Math.PI radians.
+
+ this.minPolarAngle = 0; // radians
+
+ this.maxPolarAngle = Math.PI; // radians
+ // How far you can orbit horizontally, upper and lower limits.
+ // If set, the interval [ min, max ] must be a sub-interval of [ - 2 PI, 2 PI ], with ( max - min < 2 PI )
+
+ this.minAzimuthAngle = - Infinity; // radians
+
+ this.maxAzimuthAngle = Infinity; // radians
+ // Set to true to enable damping (inertia)
+ // If damping is enabled, you must call controls.update() in your animation loop
+
+ this.enableDamping = false;
+ this.dampingFactor = 0.05; // This option actually enables dollying in and out; left as "zoom" for backwards compatibility.
+ // Set to false to disable zooming
+
+ this.enableZoom = true;
+ this.zoomSpeed = 1.0; // Set to false to disable rotating
+
+ this.enableRotate = true;
+ this.rotateSpeed = 1.0; // Set to false to disable panning
+
+ this.enablePan = true;
+ this.panSpeed = 1.0;
+ this.screenSpacePanning = true; // if false, pan orthogonal to world-space direction camera.up
+
+ this.keyPanSpeed = 7.0; // pixels moved per arrow key push
+ // Set to true to automatically rotate around the target
+ // If auto-rotate is enabled, you must call controls.update() in your animation loop
+
+ this.autoRotate = false;
+ this.autoRotateSpeed = 2.0; // 30 seconds per orbit when fps is 60
+ // The four arrow keys
+
+ this.keys = {
+ LEFT: 'ArrowLeft',
+ UP: 'ArrowUp',
+ RIGHT: 'ArrowRight',
+ BOTTOM: 'ArrowDown'
+ }; // Mouse buttons
+
+ this.mouseButtons = {
+ LEFT: THREE.MOUSE.ROTATE,
+ MIDDLE: THREE.MOUSE.DOLLY,
+ RIGHT: THREE.MOUSE.PAN
+ }; // Touch fingers
+
+ this.touches = {
+ ONE: THREE.TOUCH.ROTATE,
+ TWO: THREE.TOUCH.DOLLY_PAN
+ }; // for reset
+
+ this.target0 = this.target.clone();
+ this.position0 = this.object.position.clone();
+ this.zoom0 = this.object.zoom; // the target DOM element for key events
+
+ this._domElementKeyEvents = null; //
+ // public methods
+ //
+
+ this.getPolarAngle = function () {
+
+ return spherical.phi;
+
+ };
+
+ this.getAzimuthalAngle = function () {
+
+ return spherical.theta;
+
+ };
+
+ this.listenToKeyEvents = function ( domElement ) {
+
+ domElement.addEventListener( 'keydown', onKeyDown );
+ this._domElementKeyEvents = domElement;
+
+ };
+
+ this.saveState = function () {
+
+ scope.target0.copy( scope.target );
+ scope.position0.copy( scope.object.position );
+ scope.zoom0 = scope.object.zoom;
+
+ };
+
+ this.reset = function () {
+
+ scope.target.copy( scope.target0 );
+ scope.object.position.copy( scope.position0 );
+ scope.object.zoom = scope.zoom0;
+ scope.object.updateProjectionMatrix();
+ scope.dispatchEvent( _changeEvent );
+ scope.update();
+ state = STATE.NONE;
+
+ }; // this method is exposed, but perhaps it would be better if we can make it private...
+
+
+ this.update = function () {
+
+ const offset = new THREE.Vector3(); // so camera.up is the orbit axis
+
+ const quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) );
+ const quatInverse = quat.clone().invert();
+ const lastPosition = new THREE.Vector3();
+ const lastQuaternion = new THREE.Quaternion();
+ const twoPI = 2 * Math.PI;
+ return function update() {
+
+ const position = scope.object.position;
+ offset.copy( position ).sub( scope.target ); // rotate offset to "y-axis-is-up" space
+
+ offset.applyQuaternion( quat ); // angle from z-axis around y-axis
+
+ spherical.setFromVector3( offset );
+
+ if ( scope.autoRotate && state === STATE.NONE ) {
+
+ rotateLeft( getAutoRotationAngle() );
+
+ }
+
+ if ( scope.enableDamping ) {
+
+ spherical.theta += sphericalDelta.theta * scope.dampingFactor;
+ spherical.phi += sphericalDelta.phi * scope.dampingFactor;
+
+ } else {
+
+ spherical.theta += sphericalDelta.theta;
+ spherical.phi += sphericalDelta.phi;
+
+ } // restrict theta to be between desired limits
+
+
+ let min = scope.minAzimuthAngle;
+ let max = scope.maxAzimuthAngle;
+
+ if ( isFinite( min ) && isFinite( max ) ) {
+
+ if ( min < - Math.PI ) min += twoPI; else if ( min > Math.PI ) min -= twoPI;
+ if ( max < - Math.PI ) max += twoPI; else if ( max > Math.PI ) max -= twoPI;
+
+ if ( min <= max ) {
+
+ spherical.theta = Math.max( min, Math.min( max, spherical.theta ) );
+
+ } else {
+
+ spherical.theta = spherical.theta > ( min + max ) / 2 ? Math.max( min, spherical.theta ) : Math.min( max, spherical.theta );
+
+ }
+
+ } // restrict phi to be between desired limits
+
+
+ spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) );
+ spherical.makeSafe();
+ spherical.radius *= scale; // restrict radius to be between desired limits
+
+ spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); // move target to panned location
+
+ if ( scope.enableDamping === true ) {
+
+ scope.target.addScaledVector( panOffset, scope.dampingFactor );
+
+ } else {
+
+ scope.target.add( panOffset );
+
+ }
+
+ offset.setFromSpherical( spherical ); // rotate offset back to "camera-up-vector-is-up" space
+
+ offset.applyQuaternion( quatInverse );
+ position.copy( scope.target ).add( offset );
+ scope.object.lookAt( scope.target );
+
+ if ( scope.enableDamping === true ) {
+
+ sphericalDelta.theta *= 1 - scope.dampingFactor;
+ sphericalDelta.phi *= 1 - scope.dampingFactor;
+ panOffset.multiplyScalar( 1 - scope.dampingFactor );
+
+ } else {
+
+ sphericalDelta.set( 0, 0, 0 );
+ panOffset.set( 0, 0, 0 );
+
+ }
+
+ scale = 1; // update condition is:
+ // min(camera displacement, camera rotation in radians)^2 > EPS
+ // using small-angle approximation cos(x/2) = 1 - x^2 / 8
+
+ if ( zoomChanged || lastPosition.distanceToSquared( scope.object.position ) > EPS || 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) {
+
+ scope.dispatchEvent( _changeEvent );
+ lastPosition.copy( scope.object.position );
+ lastQuaternion.copy( scope.object.quaternion );
+ zoomChanged = false;
+ return true;
+
+ }
+
+ return false;
+
+ };
+
+ }();
+
+ this.dispose = function () {
+
+ scope.domElement.removeEventListener( 'contextmenu', onContextMenu );
+ scope.domElement.removeEventListener( 'pointerdown', onPointerDown );
+ scope.domElement.removeEventListener( 'wheel', onMouseWheel );
+ scope.domElement.removeEventListener( 'touchstart', onTouchStart );
+ scope.domElement.removeEventListener( 'touchend', onTouchEnd );
+ scope.domElement.removeEventListener( 'touchmove', onTouchMove );
+ scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove );
+ scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp );
+
+ if ( scope._domElementKeyEvents !== null ) {
+
+ scope._domElementKeyEvents.removeEventListener( 'keydown', onKeyDown );
+
+ } //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here?
+
+ }; //
+ // internals
+ //
+
+
+ const scope = this;
+ const STATE = {
+ NONE: - 1,
+ ROTATE: 0,
+ DOLLY: 1,
+ PAN: 2,
+ TOUCH_ROTATE: 3,
+ TOUCH_PAN: 4,
+ TOUCH_DOLLY_PAN: 5,
+ TOUCH_DOLLY_ROTATE: 6
+ };
+ let state = STATE.NONE;
+ const EPS = 0.000001; // current position in spherical coordinates
+
+ const spherical = new THREE.Spherical();
+ const sphericalDelta = new THREE.Spherical();
+ let scale = 1;
+ const panOffset = new THREE.Vector3();
+ let zoomChanged = false;
+ const rotateStart = new THREE.Vector2();
+ const rotateEnd = new THREE.Vector2();
+ const rotateDelta = new THREE.Vector2();
+ const panStart = new THREE.Vector2();
+ const panEnd = new THREE.Vector2();
+ const panDelta = new THREE.Vector2();
+ const dollyStart = new THREE.Vector2();
+ const dollyEnd = new THREE.Vector2();
+ const dollyDelta = new THREE.Vector2();
+
+ function getAutoRotationAngle() {
+
+ return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
+
+ }
+
+ function getZoomScale() {
+
+ return Math.pow( 0.95, scope.zoomSpeed );
+
+ }
+
+ function rotateLeft( angle ) {
+
+ sphericalDelta.theta -= angle;
+
+ }
+
+ function rotateUp( angle ) {
+
+ sphericalDelta.phi -= angle;
+
+ }
+
+ const panLeft = function () {
+
+ const v = new THREE.Vector3();
+ return function panLeft( distance, objectMatrix ) {
+
+ v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix
+
+ v.multiplyScalar( - distance );
+ panOffset.add( v );
+
+ };
+
+ }();
+
+ const panUp = function () {
+
+ const v = new THREE.Vector3();
+ return function panUp( distance, objectMatrix ) {
+
+ if ( scope.screenSpacePanning === true ) {
+
+ v.setFromMatrixColumn( objectMatrix, 1 );
+
+ } else {
+
+ v.setFromMatrixColumn( objectMatrix, 0 );
+ v.crossVectors( scope.object.up, v );
+
+ }
+
+ v.multiplyScalar( distance );
+ panOffset.add( v );
+
+ };
+
+ }(); // deltaX and deltaY are in pixels; right and down are positive
+
+
+ const pan = function () {
+
+ const offset = new THREE.Vector3();
+ return function pan( deltaX, deltaY ) {
+
+ const element = scope.domElement;
+
+ if ( scope.object.isPerspectiveCamera ) {
+
+ // perspective
+ const position = scope.object.position;
+ offset.copy( position ).sub( scope.target );
+ let targetDistance = offset.length(); // half of the fov is center to top of screen
+
+ targetDistance *= Math.tan( scope.object.fov / 2 * Math.PI / 180.0 ); // we use only clientHeight here so aspect ratio does not distort speed
+
+ panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix );
+ panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix );
+
+ } else if ( scope.object.isOrthographicCamera ) {
+
+ // orthographic
+ panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix );
+ panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix );
+
+ } else {
+
+ // camera neither orthographic nor perspective
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' );
+ scope.enablePan = false;
+
+ }
+
+ };
+
+ }();
+
+ function dollyOut( dollyScale ) {
+
+ if ( scope.object.isPerspectiveCamera ) {
+
+ scale /= dollyScale;
+
+ } else if ( scope.object.isOrthographicCamera ) {
+
+ scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) );
+ scope.object.updateProjectionMatrix();
+ zoomChanged = true;
+
+ } else {
+
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+ scope.enableZoom = false;
+
+ }
+
+ }
+
+ function dollyIn( dollyScale ) {
+
+ if ( scope.object.isPerspectiveCamera ) {
+
+ scale *= dollyScale;
+
+ } else if ( scope.object.isOrthographicCamera ) {
+
+ scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) );
+ scope.object.updateProjectionMatrix();
+ zoomChanged = true;
+
+ } else {
+
+ console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' );
+ scope.enableZoom = false;
+
+ }
+
+ } //
+ // event callbacks - update the object state
+ //
+
+
+ function handleMouseDownRotate( event ) {
+
+ rotateStart.set( event.clientX, event.clientY );
+
+ }
+
+ function handleMouseDownDolly( event ) {
+
+ dollyStart.set( event.clientX, event.clientY );
+
+ }
+
+ function handleMouseDownPan( event ) {
+
+ panStart.set( event.clientX, event.clientY );
+
+ }
+
+ function handleMouseMoveRotate( event ) {
+
+ rotateEnd.set( event.clientX, event.clientY );
+ rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
+ const element = scope.domElement;
+ rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
+
+ rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
+ rotateStart.copy( rotateEnd );
+ scope.update();
+
+ }
+
+ function handleMouseMoveDolly( event ) {
+
+ dollyEnd.set( event.clientX, event.clientY );
+ dollyDelta.subVectors( dollyEnd, dollyStart );
+
+ if ( dollyDelta.y > 0 ) {
+
+ dollyOut( getZoomScale() );
+
+ } else if ( dollyDelta.y < 0 ) {
+
+ dollyIn( getZoomScale() );
+
+ }
+
+ dollyStart.copy( dollyEnd );
+ scope.update();
+
+ }
+
+ function handleMouseMovePan( event ) {
+
+ panEnd.set( event.clientX, event.clientY );
+ panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
+ pan( panDelta.x, panDelta.y );
+ panStart.copy( panEnd );
+ scope.update();
+
+ }
+
+ function handleMouseUp( ) { // no-op
+ }
+
+ function handleMouseWheel( event ) {
+
+ if ( event.deltaY < 0 ) {
+
+ dollyIn( getZoomScale() );
+
+ } else if ( event.deltaY > 0 ) {
+
+ dollyOut( getZoomScale() );
+
+ }
+
+ scope.update();
+
+ }
+
+ function handleKeyDown( event ) {
+
+ let needsUpdate = false;
+
+ switch ( event.code ) {
+
+ case scope.keys.UP:
+ pan( 0, scope.keyPanSpeed );
+ needsUpdate = true;
+ break;
+
+ case scope.keys.BOTTOM:
+ pan( 0, - scope.keyPanSpeed );
+ needsUpdate = true;
+ break;
+
+ case scope.keys.LEFT:
+ pan( scope.keyPanSpeed, 0 );
+ needsUpdate = true;
+ break;
+
+ case scope.keys.RIGHT:
+ pan( - scope.keyPanSpeed, 0 );
+ needsUpdate = true;
+ break;
+
+ }
+
+ if ( needsUpdate ) {
+
+ // prevent the browser from scrolling on cursor keys
+ event.preventDefault();
+ scope.update();
+
+ }
+
+ }
+
+ function handleTouchStartRotate( event ) {
+
+ if ( event.touches.length == 1 ) {
+
+ rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+ } else {
+
+ const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
+ const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
+ rotateStart.set( x, y );
+
+ }
+
+ }
+
+ function handleTouchStartPan( event ) {
+
+ if ( event.touches.length == 1 ) {
+
+ panStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+ } else {
+
+ const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
+ const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
+ panStart.set( x, y );
+
+ }
+
+ }
+
+ function handleTouchStartDolly( event ) {
+
+ const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+ const distance = Math.sqrt( dx * dx + dy * dy );
+ dollyStart.set( 0, distance );
+
+ }
+
+ function handleTouchStartDollyPan( event ) {
+
+ if ( scope.enableZoom ) handleTouchStartDolly( event );
+ if ( scope.enablePan ) handleTouchStartPan( event );
+
+ }
+
+ function handleTouchStartDollyRotate( event ) {
+
+ if ( scope.enableZoom ) handleTouchStartDolly( event );
+ if ( scope.enableRotate ) handleTouchStartRotate( event );
+
+ }
+
+ function handleTouchMoveRotate( event ) {
+
+ if ( event.touches.length == 1 ) {
+
+ rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+ } else {
+
+ const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
+ const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
+ rotateEnd.set( x, y );
+
+ }
+
+ rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed );
+ const element = scope.domElement;
+ rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientHeight ); // yes, height
+
+ rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight );
+ rotateStart.copy( rotateEnd );
+
+ }
+
+ function handleTouchMovePan( event ) {
+
+ if ( event.touches.length == 1 ) {
+
+ panEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY );
+
+ } else {
+
+ const x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX );
+ const y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY );
+ panEnd.set( x, y );
+
+ }
+
+ panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed );
+ pan( panDelta.x, panDelta.y );
+ panStart.copy( panEnd );
+
+ }
+
+ function handleTouchMoveDolly( event ) {
+
+ const dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
+ const dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
+ const distance = Math.sqrt( dx * dx + dy * dy );
+ dollyEnd.set( 0, distance );
+ dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) );
+ dollyOut( dollyDelta.y );
+ dollyStart.copy( dollyEnd );
+
+ }
+
+ function handleTouchMoveDollyPan( event ) {
+
+ if ( scope.enableZoom ) handleTouchMoveDolly( event );
+ if ( scope.enablePan ) handleTouchMovePan( event );
+
+ }
+
+ function handleTouchMoveDollyRotate( event ) {
+
+ if ( scope.enableZoom ) handleTouchMoveDolly( event );
+ if ( scope.enableRotate ) handleTouchMoveRotate( event );
+
+ }
+
+ function handleTouchEnd( ) { // no-op
+ } //
+ // event handlers - FSM: listen for events and reset state
+ //
+
+
+ function onPointerDown( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ switch ( event.pointerType ) {
+
+ case 'mouse':
+ case 'pen':
+ onMouseDown( event );
+ break;
+ // TODO touch
+
+ }
+
+ }
+
+ function onPointerMove( event ) {
+
+ if ( scope.enabled === false ) return;
+
+ switch ( event.pointerType ) {
+
+ case 'mouse':
+ case 'pen':
+ onMouseMove( event );
+ break;
+ // TODO touch
+
+ }
+
+ }
+
+ function onPointerUp( event ) {
+
+ switch ( event.pointerType ) {
+
+ case 'mouse':
+ case 'pen':
+ onMouseUp( event );
+ break;
+ // TODO touch
+
+ }
+
+ }
+
+ function onMouseDown( event ) {
+
+ // Prevent the browser from scrolling.
+ event.preventDefault(); // Manually set the focus since calling preventDefault above
+ // prevents the browser from setting it automatically.
+
+ scope.domElement.focus ? scope.domElement.focus() : window.focus();
+ let mouseAction;
+
+ switch ( event.button ) {
+
+ case 0:
+ mouseAction = scope.mouseButtons.LEFT;
+ break;
+
+ case 1:
+ mouseAction = scope.mouseButtons.MIDDLE;
+ break;
+
+ case 2:
+ mouseAction = scope.mouseButtons.RIGHT;
+ break;
+
+ default:
+ mouseAction = - 1;
+
+ }
+
+ switch ( mouseAction ) {
+
+ case THREE.MOUSE.DOLLY:
+ if ( scope.enableZoom === false ) return;
+ handleMouseDownDolly( event );
+ state = STATE.DOLLY;
+ break;
+
+ case THREE.MOUSE.ROTATE:
+ if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
+
+ if ( scope.enablePan === false ) return;
+ handleMouseDownPan( event );
+ state = STATE.PAN;
+
+ } else {
+
+ if ( scope.enableRotate === false ) return;
+ handleMouseDownRotate( event );
+ state = STATE.ROTATE;
+
+ }
+
+ break;
+
+ case THREE.MOUSE.PAN:
+ if ( event.ctrlKey || event.metaKey || event.shiftKey ) {
+
+ if ( scope.enableRotate === false ) return;
+ handleMouseDownRotate( event );
+ state = STATE.ROTATE;
+
+ } else {
+
+ if ( scope.enablePan === false ) return;
+ handleMouseDownPan( event );
+ state = STATE.PAN;
+
+ }
+
+ break;
+
+ default:
+ state = STATE.NONE;
+
+ }
+
+ if ( state !== STATE.NONE ) {
+
+ scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove );
+ scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp );
+ scope.dispatchEvent( _startEvent );
+
+ }
+
+ }
+
+ function onMouseMove( event ) {
+
+ if ( scope.enabled === false ) return;
+ event.preventDefault();
+
+ switch ( state ) {
+
+ case STATE.ROTATE:
+ if ( scope.enableRotate === false ) return;
+ handleMouseMoveRotate( event );
+ break;
+
+ case STATE.DOLLY:
+ if ( scope.enableZoom === false ) return;
+ handleMouseMoveDolly( event );
+ break;
+
+ case STATE.PAN:
+ if ( scope.enablePan === false ) return;
+ handleMouseMovePan( event );
+ break;
+
+ }
+
+ }
+
+ function onMouseUp( event ) {
+
+ scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove );
+ scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp );
+ if ( scope.enabled === false ) return;
+ handleMouseUp( event );
+ scope.dispatchEvent( _endEvent );
+ state = STATE.NONE;
+
+ }
+
+ function onMouseWheel( event ) {
+
+ if ( scope.enabled === false || scope.enableZoom === false || state !== STATE.NONE && state !== STATE.ROTATE ) return;
+ event.preventDefault();
+ scope.dispatchEvent( _startEvent );
+ handleMouseWheel( event );
+ scope.dispatchEvent( _endEvent );
+
+ }
+
+ function onKeyDown( event ) {
+
+ if ( scope.enabled === false || scope.enablePan === false ) return;
+ handleKeyDown( event );
+
+ }
+
+ function onTouchStart( event ) {
+
+ if ( scope.enabled === false ) return;
+ event.preventDefault(); // prevent scrolling
+
+ switch ( event.touches.length ) {
+
+ case 1:
+ switch ( scope.touches.ONE ) {
+
+ case THREE.TOUCH.ROTATE:
+ if ( scope.enableRotate === false ) return;
+ handleTouchStartRotate( event );
+ state = STATE.TOUCH_ROTATE;
+ break;
+
+ case THREE.TOUCH.PAN:
+ if ( scope.enablePan === false ) return;
+ handleTouchStartPan( event );
+ state = STATE.TOUCH_PAN;
+ break;
+
+ default:
+ state = STATE.NONE;
+
+ }
+
+ break;
+
+ case 2:
+ switch ( scope.touches.TWO ) {
+
+ case THREE.TOUCH.DOLLY_PAN:
+ if ( scope.enableZoom === false && scope.enablePan === false ) return;
+ handleTouchStartDollyPan( event );
+ state = STATE.TOUCH_DOLLY_PAN;
+ break;
+
+ case THREE.TOUCH.DOLLY_ROTATE:
+ if ( scope.enableZoom === false && scope.enableRotate === false ) return;
+ handleTouchStartDollyRotate( event );
+ state = STATE.TOUCH_DOLLY_ROTATE;
+ break;
+
+ default:
+ state = STATE.NONE;
+
+ }
+
+ break;
+
+ default:
+ state = STATE.NONE;
+
+ }
+
+ if ( state !== STATE.NONE ) {
+
+ scope.dispatchEvent( _startEvent );
+
+ }
+
+ }
+
+ function onTouchMove( event ) {
+
+ if ( scope.enabled === false ) return;
+ event.preventDefault(); // prevent scrolling
+
+ switch ( state ) {
+
+ case STATE.TOUCH_ROTATE:
+ if ( scope.enableRotate === false ) return;
+ handleTouchMoveRotate( event );
+ scope.update();
+ break;
+
+ case STATE.TOUCH_PAN:
+ if ( scope.enablePan === false ) return;
+ handleTouchMovePan( event );
+ scope.update();
+ break;
+
+ case STATE.TOUCH_DOLLY_PAN:
+ if ( scope.enableZoom === false && scope.enablePan === false ) return;
+ handleTouchMoveDollyPan( event );
+ scope.update();
+ break;
+
+ case STATE.TOUCH_DOLLY_ROTATE:
+ if ( scope.enableZoom === false && scope.enableRotate === false ) return;
+ handleTouchMoveDollyRotate( event );
+ scope.update();
+ break;
+
+ default:
+ state = STATE.NONE;
+
+ }
+
+ }
+
+ function onTouchEnd( event ) {
+
+ if ( scope.enabled === false ) return;
+ handleTouchEnd( event );
+ scope.dispatchEvent( _endEvent );
+ state = STATE.NONE;
+
+ }
+
+ function onContextMenu( event ) {
+
+ if ( scope.enabled === false ) return;
+ event.preventDefault();
+
+ } //
+
+
+ scope.domElement.addEventListener( 'contextmenu', onContextMenu );
+ scope.domElement.addEventListener( 'pointerdown', onPointerDown );
+ scope.domElement.addEventListener( 'wheel', onMouseWheel, {
+ passive: false
+ } );
+ scope.domElement.addEventListener( 'touchstart', onTouchStart, {
+ passive: false
+ } );
+ scope.domElement.addEventListener( 'touchend', onTouchEnd );
+ scope.domElement.addEventListener( 'touchmove', onTouchMove, {
+ passive: false
+ } ); // force an update at start
+
+ this.update();
+
+ }
+
+ } // This set of controls performs orbiting, dollying (zooming), and panning.
+ // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default).
+ // This is very similar to OrbitControls, another set of touch behavior
+ //
+ // Orbit - right mouse, or left mouse + ctrl/meta/shiftKey / touch: two-finger rotate
+ // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish
+ // Pan - left mouse, or arrow keys / touch: one-finger move
+
+
+ class MapControls extends OrbitControls {
+
+ constructor( object, domElement ) {
+
+ super( object, domElement );
+ this.screenSpacePanning = false; // pan orthogonal to world-space direction camera.up
+
+ this.mouseButtons.LEFT = THREE.MOUSE.PAN;
+ this.mouseButtons.RIGHT = THREE.MOUSE.ROTATE;
+ this.touches.ONE = THREE.TOUCH.PAN;
+ this.touches.TWO = THREE.TOUCH.DOLLY_ROTATE;
+
+ }
+
+ }
+
+ THREE.MapControls = MapControls;
+ THREE.OrbitControls = OrbitControls;
+
+} )();
diff --git a/help.html b/help.html
index 95371df..e814f38 100644
--- a/help.html
+++ b/help.html
@@ -526,7 +526,13 @@ diff a.txt b.txt</pre>
<tr><td><code>ai-stop</code></td><td>Cancel running AI request</td><td><code>ai-stop</code></td></tr>
<tr><td><code>ai-compress</code></td><td>Manually compress conversation context</td><td><code>ai-compress</code></td></tr>
<tr><td><code>ai-config</code></td><td>View/set AI loop config (max-turns, max-context, max-context-tokens, confirm-timeout(0=disabled), chat-lock-timeout, skill-debounce, trivial-optimization)</td><td><code>ai-config max-turns 12</code></td></tr>
- <tr><td><code>debug</code></td><td>Toggle AI debug mode</td><td><code>debug</code></td></tr>
+ <tr><td><code>debug</code></td><td>Toggle AI debug mode (ext module)</td><td><code>debug</code></td></tr>
+ <tr><td><code>debug-ai</code></td><td>Toggle AI tool-call debug panel</td><td><code>debug-ai</code></td></tr>
+ <tr><td><code>debug-ai-last</code></td><td>Show last AI response (content, tool calls, errors)</td><td><code>debug-ai-last</code></td></tr>
+ <tr><td><code>debug-ai-send</code></td><td>Show what was sent to the AI (messages with roles)</td><td><code>debug-ai-send</code></td></tr>
+ <tr><td><code>debug-ai-log</code></td><td>Dump full tool call log for current session</td><td><code>debug-ai-log</code></td></tr>
+ <tr><td><code>archive-list</code></td><td>List archived session snapshots</td><td><code>archive-list</code></td></tr>
+ <tr><td><code>archive-read</code></td><td>View an archive by filename</td><td><code>archive-read 2026-06-25-14-30.json</code></td></tr>
<tr><td><code>mem-debug</code></td><td>Memory diagnostic (tool call log, heap, localStorage sizes)</td><td><code>mem-debug</code></td></tr>
<tr><td><code>reset-mem</code></td><td>Clear AI internal logs without touching VFS/config</td><td><code>reset-mem</code></td></tr>
</table></div>
diff --git a/main.html b/main.html
index 7f3c4a6..f383303 100644
--- a/main.html
+++ b/main.html
@@ -1342,13 +1342,13 @@
</script>
<!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
-<script src="modules/core/core-bundle.js?v=2026-06-24-v4.5.80"></script>
+<script src="modules/core/core-bundle.js?v=2026-06-30-v4.5.83"></script>
<!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules -->
-<script src="modules/core/module-loader.js?v=2026-06-24-v4.5.80"></script>
+<script src="modules/core/module-loader.js?v=2026-06-30-v4.5.83"></script>
<!-- Critical ext bundle loads AFTER webx11 exists so registerModule calls work -->
-<script src="modules/ext/critical-ext-bundle.js?v=2026-06-24-v4.5.80"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=2026-06-30-v4.5.83"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
diff --git a/modules-index.json b/modules-index.json
index 289946c..ffa4efd 100644
--- a/modules-index.json
+++ b/modules-index.json
@@ -1,6 +1,6 @@
{
- "version": "2026-06-18-v4.5.79",
- "generated": "2026-06-18T00:00:00.000Z",
+ "version": "2026-06-30-v4.5.83",
+ "generated": "2026-06-30T00:00:00.000Z",
"modules": {
"module-agent-registry": {
"name": "module-agent-registry",
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index 6f94a44..d576f14 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -1018,6 +1018,11 @@ window.VirtualFileSystem = VirtualFileSystem;
}
async loadModuleFromURL(url) {
try {
+ // Security: block cross-origin URLs for module loading
+ if (url && url.match(/^https?:\/\//) && !url.startsWith(window.location.origin)) {
+ this.terminal.print(`⚠️ Security: only same-origin URLs allowed for module loading`, 'error');
+ return false;
+ }
this.terminal.print(`Loading module from ${url}...`, 'info');
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
@@ -2000,7 +2005,15 @@ class OllamaManager {
// DeepSeek: empty response with no tools — retry once with stream forcing
if (isDeepSeek && content === '' && (!toolCalls || toolCalls.length === 0)) {
+ this.terminal.log('WARN', 'Ollama', 'DeepSeek empty response — model=' + useModel + ', messages=' + messages.length + ', tools=' + (tools ? tools.length : 0));
this.terminal.print('⚠️ DeepSeek returned empty — retrying with stream...', 'warning');
+ // Auto-truncate context: keep system prompt + last 5 messages
+ if (messages.length > 6) {
+ var sysMsg = messages[0];
+ var recentMsgs = messages.slice(-5);
+ body.messages = [sysMsg].concat(recentMsgs);
+ this.terminal.print('📏 Context truncated from ' + messages.length + ' to ' + body.messages.length + ' messages for retry', 'info');
+ }
body.stream = true;
delete body.stop;
this._abortController = new AbortController();
@@ -2035,12 +2048,16 @@ class OllamaManager {
}
}
if (fullText) {
+ this.terminal.log('INFO', 'Ollama', 'DeepSeek stream retry succeeded: ' + fullText.length + ' chars');
return {
message: { role: 'assistant', content: fullText },
tool_calls: null,
error: null
};
}
+ this.terminal.log('WARN', 'Ollama', 'DeepSeek stream retry also returned empty');
+ } else {
+ this.terminal.log('WARN', 'Ollama', 'DeepSeek stream retry HTTP ' + retryResponse.status);
}
return {
message: { role: 'assistant', content: '' },
@@ -3720,7 +3737,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
} catch (e) {}
}
async _loadExtModules(filterKeys) {
- const autoLoadKeys = filterKeys || ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console', 'media'];
+ const autoLoadKeys = filterKeys || ['debug-panel'];
const cacheVer = localStorage.getItem('webx11_cache_ver') || '2026-05-27-14';
let index = null;
let lastErr = null;
@@ -3743,6 +3760,9 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
return;
}
+ // Cache packages index for lazy-load dispatch
+ try { localStorage.setItem('webx11_packages_index', JSON.stringify(index)); } catch (e) {}
+
for (const key of autoLoadKeys) {
const pkg = index.packages[key];
if (pkg) {
@@ -3753,7 +3773,7 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
}
async reloadPersistedModules() {
const registry = this.moduleManager.moduleRegistry;
- const skipList = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console', 'media'];
+ const skipList = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'editor', 'diff', 'alias', 'finder', 'filemgr', 'cryptogzip', 'shell', 'tutorial', 'busybox', 'floating', 'console', 'media', 'three'];
for (const mod of registry) {
if (mod.enabled && mod.url) {
// Skip ext modules — they load via _loadExtModules
@@ -4131,6 +4151,40 @@ Only output XML function calls for tool tasks. If no tool is needed, answer norm
}
}
// Enhanced: check fuzzy suggestions before showing "not found"
+ // Lazy-load: check if command belongs to an unloaded ext module
+ if (!this._extModuleCommandMap) {
+ this._extModuleCommandMap = {};
+ this._extModuleUrlMap = {};
+ try {
+ const idx = JSON.parse(localStorage.getItem('webx11_packages_index') || '{}');
+ if (idx.packages) {
+ for (const [key, pkg] of Object.entries(idx.packages)) {
+ if (pkg.commands && Array.isArray(pkg.commands)) {
+ for (const c of pkg.commands) {
+ this._extModuleCommandMap[c] = key;
+ this._extModuleUrlMap[key] = pkg.url;
+ }
+ }
+ }
+ }
+ } catch (e) {}
+ }
+ if (this._extModuleCommandMap[cmd] && !this.moduleManager.modules[this._extModuleCommandMap[cmd]]) {
+ const key = this._extModuleCommandMap[cmd];
+ const url = this._extModuleUrlMap[key];
+ if (url) {
+ this.print('⏳ Loading module for "' + cmd + '"...', 'info');
+ const loaded = await this.moduleManager.loadModuleFromURL(url);
+ if (loaded) {
+ // Retry dispatch
+ const mod = this.moduleManager.modules[key];
+ if (mod && mod.commands && mod.commands[cmd]) {
+ try { await mod.commands[cmd](args); return; }
+ catch (err) { this.print('Error: ' + err.message, 'error'); return; }
+ }
+ }
+ }
+ }
const suggestion = this.security.suggest(cmd);
if (suggestion && suggestion.dist <= 2 && suggestion.dist > 0) {
this.print(`Command not found: ${cmd}`, 'error');
@@ -7032,22 +7086,24 @@ class OllamaModule {
this.terminal.print(`✅ Module '${name}' marked as unloaded (commands will not be available)`, 'success');
};
- // module-reload <name> — unload + reload with diff
- this.commands['module-reload'] = (args) => {
+ // module-reload <name> — re-fetch, re-parse, re-register
+ this.commands['module-reload'] = async (args) => {
if (!args[0]) return this.terminal.print('Usage: module-reload <name>', 'warning');
const name = args[0];
const mod = this._getMod(name);
if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error');
- const wasLoaded = mod.loaded !== false;
- const wasLazy = mod.lazy;
- this.terminal.print(`🔄 Reloading '${name}' (was: ${wasLoaded ? 'loaded' : 'unloaded'}, lazy=${wasLazy})`, 'info');
- mod.loaded = false;
- setTimeout(() => {
- mod.loaded = true;
- this.terminal.print(`✅ Module '${name}' reloaded`, 'success');
- const cmdCount = mod.commands ? Object.keys(mod.commands).length : 0;
- this.terminal.print(` Now: loaded=true, lazy=${mod.lazy}, ${cmdCount} commands`, 'info');
- }, 100);
+ const mm = this.terminal.moduleManager;
+ const regEntry = mm.moduleRegistry.find(m => m.name === name);
+ const url = regEntry ? regEntry.url : null;
+ if (!url) return this.terminal.print(`Module '${name}' has no URL — cannot reload (built-in)`, 'warning');
+ this.terminal.print(`🔄 Reloading '${name}' from ${url}...`, 'info');
+ mm.unregisterModule(name);
+ const success = await mm.loadModuleFromURL(url);
+ if (success) {
+ this.terminal.print(`✅ Module '${name}' reloaded successfully`, 'success');
+ } else {
+ this.terminal.print(`❌ Failed to reload '${name}'`, 'error');
+ }
};
// module-reset <name> — clear module state and reload
@@ -7648,6 +7704,8 @@ class ServerMonitorModule {
this._toolDefs = null;
this._toolCallLog = [];
this._debugPanelEnabled = false;
+ this._lastRawResponse = null;
+ this._lastRawMessages = null;
// 🔒 Guardrail Layer
this._guardrailPolicy = null;
this._shadowSnapshot = null;
@@ -8051,7 +8109,7 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'smart_context_list',
- description: 'List available AI context profiles (e.g. coding, debug, creative, technical). Returns profile names and system prompts.',
+ description: 'List available AI context profiles (e.g. coding, debug, creative, technical, chat, system-admin, research, security). Each profile sets the AI system prompt, temperature, and behavior for a specific task type. Also switches the agent loop profile (max turns, tool set) to match. Returns profile names, descriptions, and system prompts. Use this when the user task has a clear domain (coding, debugging, research, etc.) to optimize AI behavior.',
parameters: {
type: 'object',
properties: {},
@@ -8062,11 +8120,11 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'smart_context_set',
- description: "Set the active AI context profile. Changes the AI's behavior (temperature, style) for subsequent responses.",
+ description: "Set the active AI context profile. Changes the AI system prompt, temperature, and behavior for subsequent responses. Also switches the agent loop profile (max turns, tool set) to match. Available profiles: coding (code editing), debug (bug fixing), creative (brainstorming), technical (documentation), chat (casual), system-admin (maintenance), research (information gathering), security (threat assessment). Use this when the user task changes domain.",
parameters: {
type: 'object',
properties: {
- name: { type: 'string', description: 'Profile name (e.g. "coding", "debug", "creative", "technical", "chat")' }
+ name: { type: 'string', description: 'Profile name (e.g. "coding", "debug", "creative", "technical", "chat", "system-admin", "research", "security")' }
},
required: ['name']
}
@@ -8088,6 +8146,30 @@ buildSystemPrompt() {
}, {
type: 'function',
function: {
+ name: 'create_3d_scene',
+ description: 'Create a 3D scene with Three.js objects. Supports: boxes, spheres, torus, text, particles, lines. Also supports presets: "solar" (solar system), "grid" (color grid). Describe what you want in natural language and the module will parse it.',
+ parameters: {
+ type: 'object',
+ properties: {
+ description: { type: 'string', description: 'Natural language description of the 3D scene (e.g. "a red box and two blue spheres with particles")' }
+ },
+ required: ['description']
+ }
+ }
+ }, {
+ type: 'function',
+ function: {
+ name: 'visualize_mindmap_3d',
+ description: 'Render the current mindmap as an interactive 3D force-directed graph with colored spheres and curved connecting lines. Requires MindmapModule to have an active mindmap.',
+ parameters: {
+ type: 'object',
+ properties: {},
+ required: []
+ }
+ }
+ }, {
+ type: 'function',
+ function: {
name: 'vaultmind_query',
description: "Query VaultMind AI for Hyperliquid market data (prices, portfolio, transactions, copy-trade) or ask an AI agent. Uses same-origin /vmai/ API.",
parameters: {
@@ -8196,6 +8278,13 @@ buildSystemPrompt() {
this.commands['bench-compare'] = (args) => this.benchCompareCmd(args);
this.commands['debug'] = () => this.toggleDebugPanel();
this.commands['debug-ai'] = () => this.toggleDebug();
+ this.commands['debug-ai-last'] = () => this._showLastRawResponse();
+ this.commands['debug-ai-send'] = () => this._showLastRawRequest();
+ this.commands['debug-ai-log'] = () => this._showToolCallLog();
+ this.commands['archive-list'] = () => this._archiveList();
+ this.commands['archive-read'] = (args) => this._archiveRead(args);
+ this.commands['workflow-list'] = () => this._workflowList();
+ this.commands['workflow-run'] = (args) => this._workflowRun(args);
this.commands['guardrail'] = (args) => this.guardrailCmd(args);
this.commands['guardrail-add'] = (args) => this.guardrailCmd(['add', ...args]);
this.commands['guardrail-remove'] = (args) => this.guardrailCmd(['remove', ...args]);
@@ -8336,9 +8425,14 @@ buildSystemPrompt() {
// Log the AI interaction
this.terminal.log('INFO', 'AI', 'Query: ' + userMessage.substring(0, 100));
if (loopResult && loopResult.toolTurns) {
- this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s)');
+ var toolNames = this._toolCallLog.slice(-(loopResult.toolTurns || 0)).map(function(e) { return e.name; }).join(', ');
+ this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s): ' + toolNames);
}
}
+ // Auto-archive every 10 messages
+ if (this._messageCount > 0 && this._messageCount % 10 === 0) {
+ this._archiveSession('Auto-archive at message ' + this._messageCount);
+ }
// Register session save on page unload (runs once per session)
if (!this._sessionSaved) {
this._sessionSaved = true;
@@ -8370,12 +8464,13 @@ buildSystemPrompt() {
if (!this._debugPanelEnabled) return;
const panel = document.getElementById('debug-ai-panel');
if (!panel) return;
+ const activeTab = panel.getAttribute('data-tab') || 'tools';
const recent = this._toolCallLog.slice(-20);
- let html = '';
+ let toolHtml = '';
for (const entry of recent) {
const statusClass = entry.error ? 'debug-entry-error' : 'debug-entry-ok';
const argsStr = typeof entry.args === 'string' ? entry.args.substring(0, 60) : JSON.stringify(entry.args).substring(0, 60);
- html += '<div class="debug-entry ' + statusClass + '">' +
+ toolHtml += '<div class="debug-entry ' + statusClass + '">' +
'<span class="debug-turn">#' + entry.turn + '</span> ' +
'<span class="debug-name">' + this._escapeHtml(entry.name) + '</span> ' +
'<span class="debug-args">' + this._escapeHtml(argsStr) + '</span> ' +
@@ -8383,14 +8478,41 @@ buildSystemPrompt() {
(entry.error ? ' <span class="debug-err">⚠️</span>' : '') +
'</div>';
}
+ // Raw I/O tab content
+ let rawHtml = '';
+ if (this._lastRawResponse) {
+ const r = this._lastRawResponse;
+ const content = (r.message && r.message.content) || '';
+ const hasTools = r.tool_calls && r.tool_calls.length > 0;
+ rawHtml += '<div style="padding:6px 8px;font-size:0.75em;line-height:1.3">';
+ if (r.error) rawHtml += '<div style="color:var(--error-color)">⚠️ ' + this._escapeHtml(r.error) + '</div>';
+ if (content) {
+ const safe = this._escapeHtml(content.length > 300 ? content.substring(0, 300) + '...' : content);
+ rawHtml += '<div><strong>Content:</strong> ' + safe + '</div>';
+ }
+ if (hasTools) {
+ rawHtml += '<div><strong>Tools:</strong> ' + r.tool_calls.map(function(tc) { return tc.function.name; }).join(', ') + '</div>';
+ }
+ if (this._lastRawMessages) {
+ rawHtml += '<div style="margin-top:4px;color:var(--text-muted)">Messages: ' + this._lastRawMessages.length + '</div>';
+ }
+ rawHtml += '</div>';
+ } else {
+ rawHtml = '<div style="padding:8px;color:var(--text-muted);font-size:0.75em">No AI response yet. Send a message first.</div>';
+ }
+ const tabBar = '<div style="display:flex;border-bottom:1px solid #333">' +
+ '<span style="cursor:pointer;padding:4px 10px;font-size:0.8em;' + (activeTab === 'tools' ? 'background:var(--bg-tertiary);border-bottom:2px solid var(--success-color);font-weight:bold' : 'color:var(--text-muted)') + '" onclick="var p=document.getElementById(\'debug-ai-panel\');p.setAttribute(\'data-tab\',\'tools\');var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m)m._updateDebugPanel()">🔧 Tools</span>' +
+ '<span style="cursor:pointer;padding:4px 10px;font-size:0.8em;' + (activeTab === 'raw' ? 'background:var(--bg-tertiary);border-bottom:2px solid var(--success-color);font-weight:bold' : 'color:var(--text-muted)') + '" onclick="var p=document.getElementById(\'debug-ai-panel\');p.setAttribute(\'data-tab\',\'raw\');var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m)m._updateDebugPanel()">📡 Raw I/O</span>' +
+ '</div>';
+ const bodyHtml = activeTab === 'raw' ? rawHtml : toolHtml;
panel.innerHTML = '<div class="debug-header" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;border-bottom:1px solid #333;font-weight:bold;color:var(--success-color);font-size:0.9em;flex-wrap:wrap;gap:4px;">' +
- '<span>🔧 Tool Log <span class="debug-count" style="color:var(--text-muted);font-weight:normal;font-size:0.85em;">' + this._toolCallLog.length + ' calls</span></span>' +
+ '<span>🔧 AI Debug <span class="debug-count" style="color:var(--text-muted);font-weight:normal;font-size:0.85em;">' + this._toolCallLog.length + ' calls</span></span>' +
'<span style="display:flex;gap:4px;">' +
'<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var t=m._toolCallLog.map(function(e){return\'#\'+e.turn+\' \'+e.name+\' (\'+e.duration+\'s)\'+(e.error?\' ⚠️\':\'\')+\'\\n\'+JSON.stringify(e.args).substring(0,80)}).join(\'\\n\\n\');navigator.clipboard.writeText(t).then(function(){m.terminal&&m.terminal.print(\'📋 Debug log copied to clipboard\',\'success\')}).catch(function(){prompt(\'Copy debug log:\',t)})}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📋</button>' +
'<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var json=JSON.stringify(m._toolCallLog,null,2);var b=new Blob([json],{type:\'application/json\'});var u=URL.createObjectURL(b);var a=document.createElement(\'a\');a.href=u;a.download=\'debug-log-\'+new Date().toISOString().slice(0,19)+\'.json\';document.body.appendChild(a);a.click();setTimeout(function(){document.body.removeChild(a);URL.revokeObjectURL(u)},2000);m.terminal&&m.terminal.print(\'📥 Debug log exported\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📥</button>' +
'<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var existing=[];var raw=m.terminal.fs.readFile(\'/debug.log\');if(raw){try{existing=JSON.parse(raw);}catch(e){}}var log=m._toolCallLog;var seen={};existing.forEach(function(e){seen[e.turn]=true;});log.forEach(function(e){if(!seen[e.turn]){existing.push(e);seen[e.turn]=true;}});var json=JSON.stringify(existing,null,2);m.terminal.fs.writeFile(\'/debug.log\',json);m.terminal.print(\'💾 Debug log appended — \'+existing.length+\' entries total\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">💾</button>' +
'<span style="cursor:pointer;color:var(--text-muted);font-size:1.1em;touch-action:manipulation;" onclick="var p=document.getElementById(\'debug-ai-panel\');if(p)p.style.display=\'none\';var a=window.webx11&&webx11.moduleManager&&webx11.moduleManager.modules&&webx11.moduleManager.modules.WebX11AIAssistantModule;if(a)a._debugPanelEnabled=false;">✕</span>' +
- '</span></div>' + html;
+ '</span></div>' + tabBar + bodyHtml;
}
_isToolError(result) {
if (!result || typeof result !== 'string') return null;
@@ -8441,6 +8563,28 @@ buildSystemPrompt() {
var loopDetect = loopCfg.loopDetection || { enabled: false, threshold: 6, windowMs: 30000 };
var confEsc = loopCfg.confidenceEscalation || { enabled: true, threshold: 90 };
var finalPrompt = loopCfg.finalTurnPrompt || 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.';
+ // Memory-augmented planning: inject prior loop lessons as negative constraints
+ try {
+ var fs = this.terminal.fs;
+ if (fs) {
+ var index = fs.fsReadJSON('/skills/index.json') || {};
+ var lessonKeys = Object.keys(index).filter(function(k) { return k.indexOf('loop-lesson-') === 0; });
+ if (lessonKeys.length > 0) {
+ var recentLessons = lessonKeys.sort().slice(-3);
+ var lessonTexts = [];
+ for (var li = 0; li < recentLessons.length; li++) {
+ var lc = fs.readFile('/skills/' + recentLessons[li] + '.md');
+ if (lc) {
+ var excerpt = lc.substring(0, 300);
+ lessonTexts.push('[Lesson: ' + recentLessons[li] + '] ' + excerpt);
+ }
+ }
+ if (lessonTexts.length > 0) {
+ messages.push({ role: 'system', content: 'Prior loop lessons (read before planning):\n' + lessonTexts.join('\n---\n') + '\n\nUse these to avoid repeating past mistakes. If lessons mention tool failures, verify those tools before calling them.' });
+ }
+ }
+ }
+ } catch (e) { /* VFS not available — skip memory-augmented planning */ }
let toolTurns = 0;
const _seenCalls = new Map();
const _loopWindow = loopDetect.windowMs || 30000;
@@ -8455,6 +8599,9 @@ buildSystemPrompt() {
const thinkStart = Date.now();
const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions());
const thinkTime = Math.round((Date.now() - thinkStart) / 1000);
+ // Store raw response for debug-ai-last
+ this._lastRawResponse = res;
+ this._lastRawMessages = messages.slice();
if (this._cancelled) return { finalAnswer: null, toolTurns };
if (res.error) { this.terminal.print(`⚠️ AI model error (${thinkTime}s): ${res.error}`, 'error'); return { finalAnswer: null, toolTurns }; }
if (res.message && !res.tool_calls) {
@@ -9259,12 +9406,33 @@ buildSystemPrompt() {
const fs = this.terminal.fs;
const mm = this.terminal.moduleManager;
const om = this.terminal.ollamaManager;
+ var agentInfo = {};
+ try {
+ var orch = mm.modules['OrchestratorModule'];
+ var agentReg = mm.modules['AgentRegistryModule'];
+ if (orch) {
+ var agents = orch._agents || {};
+ var busy = 0, failed = 0;
+ for (var ak in agents) {
+ var s = agents[ak].status;
+ if (s === 'busy') busy++;
+ if (s === 'failed' || s === 'restarting') failed++;
+ }
+ agentInfo.activeAgents = Object.keys(agents).length;
+ agentInfo.busyAgents = busy;
+ agentInfo.failedAgents = failed;
+ }
+ if (agentReg) {
+ agentInfo.registeredAgents = Object.keys(agentReg._agents || {}).length;
+ }
+ } catch (e) {}
return JSON.stringify({
files: fs.ls().length,
dirs: fs.currentPath.length ? '/' + fs.currentPath.join('/') : '/',
modules: Object.keys(mm.modules).length,
ollama: om.connected ? `Connected (${om.currentModel})` : 'Disconnected',
- memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A'
+ memory: performance.memory ? (performance.memory.usedJSHeapSize / 1048576).toFixed(1) + 'MB' : 'N/A',
+ agents: agentInfo
});
}
case 'get_help': {
@@ -9631,10 +9799,26 @@ buildSystemPrompt() {
return JSON.stringify({ error: `Profile '${name}' not found`, available: Object.keys(scm.contextProfiles) });
}
scm.activeProfile = name;
+ scm.saveActiveProfile();
const profile = scm.contextProfiles[name];
// Rebuild system prompt to reflect new context profile
this.systemPrompt = this.buildSystemPrompt();
- return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature });
+ // Also switch agent loop profile to match
+ const PROFILE_MAP = {
+ 'coding': 'coder',
+ 'debug': 'minimal',
+ 'creative': 'creative',
+ 'technical': 'research',
+ 'chat': 'default',
+ 'system-admin': 'safe',
+ 'research': 'research',
+ 'security': 'safe'
+ };
+ const targetLoop = PROFILE_MAP[name];
+ if (targetLoop && this._switchLoopProfile) {
+ this._switchLoopProfile(targetLoop);
+ }
+ return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature, agentLoop: targetLoop || null });
}
case 'create_mindmap': {
const mindmap = this.terminal.moduleManager.modules['MindmapModule'];
@@ -9657,6 +9841,26 @@ buildSystemPrompt() {
mindmap._svg([]);
return JSON.stringify({ success: true, topic, children: added.length ? added : 'none', nodes: mindmap._countNodes(cur.root), rendered: 'inline SVG in terminal' });
}
+ case 'create_3d_scene': {
+ const three = this.terminal.moduleManager.modules['ThreeModule'];
+ if (!three) return JSON.stringify({ error: 'ThreeModule not loaded' });
+ try {
+ const result = await three.createSceneFromDescription(a.description || '');
+ return JSON.stringify({ success: true, result });
+ } catch (e) {
+ return JSON.stringify({ error: '3D scene creation failed: ' + e.message });
+ }
+ }
+ case 'visualize_mindmap_3d': {
+ const three = this.terminal.moduleManager.modules['ThreeModule'];
+ if (!three) return JSON.stringify({ error: 'ThreeModule not loaded' });
+ try {
+ const result = await three.visualizeMindmap3D(a.name || '');
+ return JSON.stringify({ success: true, result });
+ } catch (e) {
+ return JSON.stringify({ error: '3D mindmap visualization failed: ' + e.message });
+ }
+ }
case 'view_module':
case 'edit_module':
case 'test_module':
@@ -10331,6 +10535,191 @@ buildSystemPrompt() {
}
return total;
}
+ _showToolCallLog() {
+ if (!this._toolCallLog || this._toolCallLog.length === 0) {
+ this.terminal.print('No tool calls recorded this session.', 'warning');
+ return;
+ }
+ let out = '── Tool Call Log (' + this._toolCallLog.length + ' calls) ──\n';
+ for (var i = 0; i < this._toolCallLog.length; i++) {
+ var e = this._toolCallLog[i];
+ var err = e.error ? ' ⚠️' : '';
+ var argsStr = typeof e.args === 'string' ? e.args.substring(0, 80) : JSON.stringify(e.args).substring(0, 80);
+ out += ' #' + e.turn + ' ' + e.name + '(' + argsStr + ') ' + e.duration + 's' + err + '\n';
+ }
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ }
+ _archiveList() {
+ try {
+ const fs = this.terminal.fs;
+ const files = fs.ls('/memory/archive') || [];
+ const jsonFiles = files.filter(function(f) { return f.name && f.name.endsWith('.json'); });
+ if (jsonFiles.length === 0) {
+ this.terminal.print('No archives found. Run ai-compress to create one.', 'warning');
+ return;
+ }
+ jsonFiles.sort(function(a, b) { return (a.name || '') < (b.name || '') ? 1 : -1; });
+ let out = '── Archives (' + jsonFiles.length + ') ──\n';
+ for (var i = 0; i < Math.min(jsonFiles.length, 30); i++) {
+ var f = jsonFiles[i];
+ var size = f.size ? '(' + f.size + 'b)' : '';
+ out += ' ' + f.name + ' ' + size + '\n';
+ }
+ out += '── End ──\nUse: archive-read <filename>';
+ this.terminal.print(out, 'info');
+ } catch (e) {
+ this.terminal.print('Error listing archives: ' + e.message, 'error');
+ }
+ }
+ _archiveRead(args) {
+ var name = (args && args[0]) || '';
+ if (!name) {
+ this.terminal.print('Usage: archive-read <filename> (e.g. 2026-06-25-14-30.json)', 'warning');
+ return;
+ }
+ try {
+ const fs = this.terminal.fs;
+ var data = fs.fsReadJSON('/memory/archive/' + name);
+ if (!data) {
+ this.terminal.print('Archive not found: ' + name, 'error');
+ return;
+ }
+ var out = '── Archive: ' + name + ' ──\n';
+ out += 'Archived: ' + (data.archivedAt || '?') + '\n';
+ out += 'Summary: ' + (data.summary || '(none)') + '\n';
+ out += 'User messages: ' + (data.user?.messageCount || 0) + '\n';
+ out += 'System: ' + (data.system?.modules || 0) + ' modules, ' + (data.system?.model || '?') + '\n';
+ out += 'Context: ' + (data.context?.messageCount || 0) + ' msgs, ~' + (data.context?.estimatedTokens || 0) + ' tokens\n';
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ } catch (e) {
+ this.terminal.print('Error reading archive: ' + e.message, 'error');
+ }
+ }
+ _workflowList() {
+ var workflows = [
+ { name: 'daily-health-report', desc: 'Schedule a daily system health report using Cron + WorldModel + ServerMonitor' },
+ { name: 'predict-and-monitor', desc: 'Predict task duration with Predictor, then schedule monitoring via Cron' },
+ { name: 'compress-archives', desc: 'Compress and archive old session files using CryptoGzip + Cron' },
+ { name: 'media-summarize', desc: 'Analyze a media file and summarize it with AI' },
+ { name: 'sandbox-test', desc: 'Run a command in the sandbox and report results' }
+ ];
+ var out = '── Workflows (' + workflows.length + ') ──\n';
+ for (var i = 0; i < workflows.length; i++) {
+ out += ' ' + workflows[i].name + ' — ' + workflows[i].desc + '\n';
+ }
+ out += '── End ──\nUse: workflow-run <name>';
+ this.terminal.print(out, 'info');
+ }
+ _workflowRun(args) {
+ var name = (args && args[0]) || '';
+ if (!name) {
+ this.terminal.print('Usage: workflow-run <name> (e.g. daily-health-report)', 'warning');
+ return;
+ }
+ var workflows = {
+ 'daily-health-report': {
+ steps: [
+ 'Run: status — check current system state',
+ 'Run: cron-every 1h "status" — schedule hourly health check',
+ 'The WorldModel will auto-synthesize system awareness each cycle'
+ ]
+ },
+ 'predict-and-monitor': {
+ steps: [
+ 'Run: predict "run system health check" — estimate duration',
+ 'Run: cron-every 30m "status" — monitor progress',
+ 'Check cron-list to see scheduled tasks'
+ ]
+ },
+ 'compress-archives': {
+ steps: [
+ 'Run: archive-list — see current archives',
+ 'Run: gzip /memory/archive/2026-06-25-*.json — compress old archives',
+ 'Run: cron-every 1d "gzip /memory/archive/*.json" — auto-compress daily'
+ ]
+ },
+ 'media-summarize': {
+ steps: [
+ 'Run: media list — see available media files',
+ 'Run: media analyze <file> — analyze with AI',
+ 'The AI will summarize the media content'
+ ]
+ },
+ 'sandbox-test': {
+ steps: [
+ 'Run: sandbox-status — check sandbox pool',
+ 'Run: sandbox-run "ls /" — test command in sandbox',
+ 'Check output for isolation verification'
+ ]
+ }
+ };
+ var wf = workflows[name];
+ if (!wf) {
+ this.terminal.print('Workflow not found: ' + name + '. Use workflow-list to see available workflows.', 'error');
+ return;
+ }
+ var out = '── Workflow: ' + name + ' ──\n';
+ for (var i = 0; i < wf.steps.length; i++) {
+ out += ' ' + (i + 1) + '. ' + wf.steps[i] + '\n';
+ }
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ }
+ _showLastRawResponse() {
+ if (!this._lastRawResponse) {
+ this.terminal.print('No AI response recorded yet. Send a message first.', 'warning');
+ return;
+ }
+ const r = this._lastRawResponse;
+ var content = (r.message && r.message.content) || '';
+ var hasTools = r.tool_calls && r.tool_calls.length > 0;
+ var toolStr = hasTools ? r.tool_calls.map(function(tc) { return tc.function.name + '(' + (tc.function.arguments || '{}') + ')'; }).join(', ') : '';
+ // Render as HTML card for better readability
+ var html = '<div class="msg-card ai" style="border:1px solid var(--border-color);border-radius:6px;margin:4px 0;overflow:hidden">' +
+ '<div class="msg-card-header" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background:var(--bg-secondary);border-bottom:1px solid var(--border-color);font-weight:bold;font-size:0.9em">' +
+ '<span>🤖 Last AI Response</span>' +
+ '<span style="display:flex;gap:4px">' +
+ '<button class="card-btn" onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m&&m._lastRawResponse){navigator.clipboard.writeText(JSON.stringify(m._lastRawResponse,null,2)).then(function(){m.terminal.print(\'📋 Raw response copied\',\'success\')})}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em">📋 Copy Raw</button>' +
+ '</span></div>' +
+ '<div class="msg-card-body" style="padding:8px 10px;font-size:0.85em;line-height:1.4">';
+ if (r.error) {
+ html += '<div style="color:var(--error-color)">⚠️ Error: ' + this._escapeHtml(r.error) + '</div>';
+ }
+ if (content) {
+ var safe = this._escapeHtml(content.length > 1000 ? content.substring(0, 1000) + '...(' + content.length + ' chars)' : content);
+ html += '<div style="margin-bottom:4px"><strong>Content:</strong><br>' + safe + '</div>';
+ } else if (!hasTools) {
+ html += '<div style="color:var(--warning-color)">⚠️ Empty response (no content, no tool calls)</div>';
+ }
+ if (hasTools) {
+ html += '<div style="margin-top:4px"><strong>Tool calls:</strong> ' + this._escapeHtml(toolStr) + '</div>';
+ }
+ html += '</div></div>';
+ this.terminal.printHTML(html);
+ }
+ _showLastRawRequest() {
+ if (!this._lastRawMessages) {
+ this.terminal.print('No AI request recorded yet. Send a message first.', 'warning');
+ return;
+ }
+ const msgs = this._lastRawMessages;
+ let out = '── Last AI Request (' + msgs.length + ' messages) ──\n';
+ for (let i = 0; i < msgs.length; i++) {
+ const m = msgs[i];
+ const role = m.role || '?';
+ const content = (m.content || '').substring(0, 200);
+ const suffix = (m.content || '').length > 200 ? '...' : '';
+ out += ' [' + i + '] ' + role + ': ' + content + suffix + '\n';
+ if (i > 8 && i < msgs.length - 2) {
+ out += ' ... (' + (msgs.length - i - 2) + ' more messages) ...\n';
+ i = msgs.length - 3;
+ }
+ }
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ }
async _compressContext() {
if (this.context.length <= 6) return;
const totalTokens = this._estimateContextTokens();
@@ -10356,10 +10745,58 @@ buildSystemPrompt() {
} catch (e) { /* fallback to truncation */ }
this.context = [{ role: 'system', content: compressed }, ...recentMsgs];
this.terminal.print('📦 Context compressed (' + oldMsgs.length + ' messages → 1 summary, ~' + totalTokens + ' tokens)', 'info');
+ // Also archive session snapshot
+ this._archiveSession(compressed);
+ }
+ _archiveSession(summary) {
+ try {
+ const fs = this.terminal.fs;
+ fs.mkdirRecursive('/memory/archive');
+ const now = new Date();
+ const pad = function(n) { return String(n).padStart(2, '0'); };
+ const ts = now.getFullYear() + '-' + pad(now.getMonth()+1) + '-' + pad(now.getDate()) + '-' + pad(now.getHours()) + '-' + pad(now.getMinutes());
+ const archive = {
+ archivedAt: now.toISOString(),
+ summary: summary || '',
+ user: {
+ profile: this._userProfile || {},
+ topics: [],
+ messageCount: this._messageCount
+ },
+ system: {
+ modules: this.terminal?.moduleManager?.modules ? Object.keys(this.terminal.moduleManager.modules).length : 0,
+ model: this.terminal?.ollamaManager?.currentModel || 'unknown',
+ provider: this.terminal?.ollamaManager?.currentProvider?.name || 'unknown'
+ },
+ context: {
+ messageCount: this.context.length,
+ estimatedTokens: this._estimateContextTokens()
+ }
+ };
+ fs.fsWriteJSON('/memory/archive/' + ts + '.json', archive);
+ // Rotation: keep last 30
+ try {
+ const files = fs.ls('/memory/archive') || [];
+ const jsonFiles = files.filter(function(f) { return f.name && f.name.endsWith('.json'); });
+ if (jsonFiles.length > 30) {
+ jsonFiles.sort(function(a, b) { return (a.name || '') < (b.name || '') ? -1 : 1; });
+ const toDelete = jsonFiles.slice(0, jsonFiles.length - 30);
+ for (var i = 0; i < toDelete.length; i++) {
+ fs.deleteFile('/memory/archive/' + toDelete[i].name);
+ }
+ }
+ } catch (e) { /* rotation best-effort */ }
+ } catch (e) { /* archive best-effort */ }
}
_manualCompress() {
this._compressContext().then(() => {
- this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens', 'info');
+ const fs = this.terminal.fs;
+ let archiveCount = 0;
+ try {
+ const files = fs.ls('/memory/archive') || [];
+ archiveCount = files.filter(function(f) { return f.name && f.name.endsWith('.json'); }).length;
+ } catch (e) {}
+ this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens, ' + archiveCount + ' archive(s)', 'info');
}).catch(e => {
this.terminal.print('Compression error: ' + e.message, 'error');
});
@@ -10466,10 +10903,21 @@ buildSystemPrompt() {
var failedTools = toolLog.filter(function(t) { return t.error; }); if (failedTools.length > 2) { var uniqueTools = []; failedTools.forEach(function(t) { if (uniqueTools.indexOf(t.name) === -1) uniqueTools.push(t.name); }); anomalies.push({ type: "tool_failures", count: failedTools.length, tools: uniqueTools }); }
var bench = this._activeLoopProfile ? this._activeLoopProfile.benchmark : null; if (bench && bench.avgTokens && usageData) { if (usageData.total_tokens > bench.avgTokens * 1.5) { anomalies.push({ type: "above_benchmark", value: usageData.total_tokens, benchmark: bench.avgTokens, ratio: (usageData.total_tokens / bench.avgTokens).toFixed(1) }); } }
if (anomalies.length === 0) return;
- var suggestions = []; for (var ai = 0; ai < anomalies.length; ai++) { var a = anomalies[ai]; if (a.type === "max_turns_near_limit") suggestions.push("This task used " + result.toolTurns + "/" + maxT + " turns — consider increasing max-turns or splitting the task"); else if (a.type === "high_token_usage") suggestions.push("High token usage (" + (a.value/1000).toFixed(0) + "K) — consider a minimal-tools profile for simpler tasks"); else if (a.type === "tool_failures") suggestions.push(a.count + " tool failures (" + a.tools.join(", ") + ") — check if modules are loaded or paths correct"); else if (a.type === "above_benchmark") suggestions.push("This run used " + a.ratio + "x more tokens than benchmark avg (" + a.benchmark + ") — consider profile optimization"); }
+ var suggestions = []; var profileAdjusted = false;
+ for (var ai = 0; ai < anomalies.length; ai++) { var a = anomalies[ai];
+ if (a.type === "max_turns_near_limit") {
+ suggestions.push("This task used " + result.toolTurns + "/" + maxT + " turns — consider increasing max-turns or splitting the task");
+ if (this._activeLoopProfile && this._activeLoopProfile.loop) { var newMax = (this._activeLoopProfile.loop.maxTurns || this.MAX_TOOL_TURNS) + 2; this._activeLoopProfile.loop.maxTurns = newMax; this._saveLoopProfile(this._activeLoopProfile); suggestions.push(" → Auto-adjusted maxTurns to " + newMax); profileAdjusted = true; }
+ } else if (a.type === "high_token_usage") suggestions.push("High token usage (" + (a.value/1000).toFixed(0) + "K) — consider a minimal-tools profile for simpler tasks");
+ else if (a.type === "tool_failures") {
+ suggestions.push(a.count + " tool failures (" + a.tools.join(", ") + ") — check if modules are loaded or paths correct");
+ if (this._activeLoopProfile && this._activeLoopProfile.systemPrompt) { var constraint = "[Constraint] Avoid tools that failed: " + a.tools.join(", ") + ". Verify module state before calling these tools."; if (this._activeLoopProfile.systemPrompt.append) { if (this._activeLoopProfile.systemPrompt.append.indexOf(constraint) === -1) { this._activeLoopProfile.systemPrompt.append += "\n" + constraint; } } else { this._activeLoopProfile.systemPrompt.append = constraint; } this._saveLoopProfile(this._activeLoopProfile); suggestions.push(" → Added constraint to profile: avoid " + a.tools.join(", ")); profileAdjusted = true; }
+ } else if (a.type === "above_benchmark") suggestions.push("This run used " + a.ratio + "x more tokens than benchmark avg (" + a.benchmark + ") — consider profile optimization");
+ }
var suggestion = suggestions.join("\n");
- try { var fs = this.terminal.fs; var index = fs.fsReadJSON("/skills/index.json") || {}; var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf("auto-improvement") !== -1; }); if (autoSkills.length < 10) { var skillName = "loop-lesson-" + Date.now(); var content = "---\nname: " + skillName + "\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n" + JSON.stringify(anomalies, null, 2) + "\n\n## Suggestions\n\n" + suggestion + "\n\n## Context\n- Profile: " + (this._activeLoopProfile ? this._activeLoopProfile.name : "none") + "\n- Turns: " + result.toolTurns + "\n- Tools used: " + toolLog.map(function(t) { return t.name; }).join(", "); fs.mkdirRecursive("/skills"); fs.writeFile("/skills/" + skillName + ".md", content); index[skillName] = { name: skillName, description: "Auto-saved loop lesson", tags: ["auto-improvement", "agent-loop", "auto"], category: "system", created: new Date().toISOString(), updated: new Date().toISOString() }; fs.fsWriteJSON("/skills/index.json", index); } } catch (e) {}
- this.terminal.print("💡 Self-improvement: " + suggestion, "info");
+ try { var fs = this.terminal.fs; var index = fs.fsReadJSON("/skills/index.json") || {}; var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf("auto-improvement") !== -1; }); if (!this._loopLessonCleaned) { this._loopLessonCleaned = true; autoSkills.forEach(function(e) { try { fs.unlink("/skills/" + e[0] + ".md"); } catch (ex) {} delete index[e[0]]; }); autoSkills = []; } var now = Date.now(); autoSkills = autoSkills.filter(function(e) { var created = new Date(e[1].created).getTime(); if (now - created > 86400000) { try { fs.unlink("/skills/" + e[0] + ".md"); } catch (ex) {} delete index[e[0]]; return false; } return true; }); var anomalyTypes = anomalies.map(function(a) { return a.type; }).sort().join(","); var isDuplicate = autoSkills.length > 0 && autoSkills[autoSkills.length - 1][1].anomalyTypes === anomalyTypes; if (!isDuplicate && autoSkills.length >= 3) { var oldest = autoSkills[0]; try { fs.unlink("/skills/" + oldest[0] + ".md"); } catch (ex) {} delete index[oldest[0]]; autoSkills.shift(); } if (!isDuplicate) { var skillName = "loop-lesson-" + Date.now(); var content = "---\nname: " + skillName + "\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n" + JSON.stringify(anomalies, null, 2) + "\n\n## Suggestions\n\n" + suggestion + "\n\n## Context\n- Profile: " + (this._activeLoopProfile ? this._activeLoopProfile.name : "none") + "\n- Turns: " + result.toolTurns + "\n- Tools used: " + toolLog.map(function(t) { return t.name; }).join(", "); fs.mkdirRecursive("/skills"); fs.writeFile("/skills/" + skillName + ".md", content); index[skillName] = { name: skillName, description: "Auto-saved loop lesson", tags: ["auto-improvement", "agent-loop", "auto"], category: "system", created: new Date().toISOString(), updated: new Date().toISOString(), anomalyTypes: anomalyTypes }; fs.fsWriteJSON("/skills/index.json", index); } } catch (e) {}
+ var msg = "💡 Self-improvement: " + anomalies.length + " anomaly(ies)"; if (profileAdjusted) msg += " — profile auto-adjusted";
+ this.terminal.print(msg, "info");
}
_vaultmindStatusCmd() {
const token = localStorage.getItem('vaultmind_token');
@@ -15858,6 +16306,12 @@ class NeuralVFSModule {
resolve(args) {
if (!args[0]) return this.terminal.print('Usage: nvfs-resolve <concept>', 'warning');
+ // Auto-sync dirty paths before resolving
+ if (this._dirtyPaths.size > 0) {
+ this.terminal.print('🔄 Auto-syncing ' + this._dirtyPaths.size + ' dirty file(s) before resolve...', 'info');
+ this._reindexDirtyPaths();
+ }
+
const concept = args.join(' ').toLowerCase();
// Check aliases first
@@ -16700,11 +17154,14 @@ if (typeof window !== 'undefined') {
}
getDefaultProfiles() {
return {
- 'coding': { name: 'Coding Assistant', systemPrompt: "You are an expert programmer.", temperature: 0.7 },
- 'debug': { name: 'Debug Helper', systemPrompt: "You are a debugging expert.", temperature: 0.5 },
- 'creative': { name: 'Creative Writer', systemPrompt: "You are a creative writer.", temperature: 0.9 },
- 'technical': { name: 'Technical Docs', systemPrompt: "You are a technical writer.", temperature: 0.3 },
- 'chat': { name: 'Casual Chat', systemPrompt: "You are a friendly assistant.", temperature: 0.8 }
+ 'coding': { name: 'Coding Assistant', systemPrompt: "You are a WebX11 newOS4 coding expert. You have access to the full terminal, VFS, and module system.\n\nAvailable commands: view_module, edit_module, test_module, reload_module, list_modified_modules, check_updates, load-local, loadmod, removemod, startup-modules, node, js-run, git, diff, patch\n\nSkills available: module-management (view/edit/test/reload modules), skills-system-guide (create/patch skills)\n\nBehavior:\n- Always show exact code changes when editing modules\n- Use edit_module for modifications, test_module to verify\n- Create checkpoints before risky edits: checkpoint mod-edit-<name>\n- Reference the module-management skill for the full workflow\n- When the user reports a bug, first view_module to inspect, then edit\n- Use nvfs_resolve to find relevant files before editing\n- Prefer write_file for creating new files, edit_module for modifying modules", temperature: 0.7 },
+ 'debug': { name: 'Debug Helper', systemPrompt: "You are a WebX11 newOS4 debugging expert. Your goal is to diagnose and fix issues systematically.\n\nAvailable commands: debug, debug-ai, debug-ai-last, debug-ai-send, debug-ai-log, logs, logrotate, health-check, check, mem, status, archive-list, archive-read, guardrail, shadow, shadow-commit, shadow-rollback\n\nSkills available: debug-panel-and-logs (toggle debug panel, session logging, diagnostics), system-maintenance (health checks, config, security)\n\nBehavior:\n- Start with health-check and check to assess system state\n- Use debug-ai to see the last AI tool call and response\n- Use logs 50 ERROR to find error entries\n- Check browser console for JS errors\n- Use guardrail to inspect security policies\n- Use shadow for safe execution of risky commands\n- Always check module loading status first", temperature: 0.3 },
+ 'creative': { name: 'Creative Writer', systemPrompt: "You are a creative assistant for WebX11 newOS4. You help with brainstorming, content creation, and design.\n\nAvailable commands: mindmap new/add/show/save/load/export, floating-add/remove/list, ai-system, ai-config\n\nSkills available: mindmap-workflow (create/view/edit mindmaps), floating-ui (create floating buttons/menus/panels)\n\nBehavior:\n- Use mindmap for brainstorming and organizing ideas\n- Use floating-add to create quick-access UI elements\n- Use web_search and web_fetch for research and inspiration\n- Higher temperature (0.9) for creative divergence\n- When the user says \"brainstorm\" or \"diagram\", create a mindmap\n- Use create_mindmap AI tool for visual architecture diagrams\n- Reference the mindmap-workflow skill for full capabilities", temperature: 0.9 },
+ 'technical': { name: 'Technical Docs', systemPrompt: "You are a technical documentation expert for WebX11 newOS4. You help with docs, guides, and system understanding.\n\nAvailable commands: help, commands, status, modules, health-check, nvfs-index, nvfs-resolve, view_module, cat, edit, grep, find, tree\n\nSkills available: navigating-terminal (explore terminal, filesystem, system state), web-search-and-fetch (search web, fetch URLs), skills-system-guide (create/patch skills)\n\nBehavior:\n- Use help and commands to discover available functionality\n- Use nvfs-resolve to find files by concept\n- Use view_module to inspect module source for documentation\n- When creating documentation, use write_file to save to VFS\n- Reference the navigating-terminal skill for filesystem exploration\n- Use web_search for external research", temperature: 0.3 },
+ 'chat': { name: 'Casual Chat', systemPrompt: "You are a friendly, helpful assistant for WebX11 newOS4. You handle general questions, casual conversation, and light tasks.\n\nAvailable commands: ai, ask, search, fetch, status, help, user_get, user_set, session_save, session_recent, my-set, my-get\n\nSkills available: memory-and-profile (user profile, session memory, checkpoints), web-search-and-fetch (search web, fetch URLs)\n\nBehavior:\n- Be conversational and approachable\n- Use user_get/user_set to remember user preferences\n- Use session_recent at start to recall past work\n- Use session_save after completing tasks\n- For complex tasks, suggest switching to a more specific context profile\n- Reference the memory-and-profile skill for persistence features", temperature: 0.8 },
+ 'system-admin': { name: 'System Admin', systemPrompt: "You are a WebX11 newOS4 system administrator. You handle maintenance, backup, security, and configuration.\n\nAvailable commands: config-list, config-set, config-delete, config-reset, security-level, security, cmd-allow, cmd-deny, backup, vfs-export, webdav-backup, webdav-restore, webdav-config, webdav-status, factory-reset, reset-vfs, checkpoint, checkpoint-list, rollback, health-check, check_updates, font-size\n\nSkills available: system-maintenance (reset, config, security, health checks), backup-and-restore (VFS backup, WebDAV cloud backup, restore), webdav-operations (cloud storage, file sync), security-and-safety (security levels, command blocking)\n\nBehavior:\n- Always create a checkpoint before making system changes\n- Use webdav-backup before destructive operations\n- Check security-level before running commands\n- Use health-check for full system audit\n- Reference the system-maintenance skill for reset procedures\n- Reference the backup-and-restore skill for data safety", temperature: 0.3 },
+ 'research': { name: 'Research Assistant', systemPrompt: "You are a research assistant for WebX11 newOS4. You gather information, analyze data, and synthesize findings.\n\nAvailable commands: search, fetch, web_search, web_fetch, nvfs-resolve, nvfs-index, grep, find, cat, view_module, session_recent, session_load, session_stats\n\nSkills available: web-search-and-fetch (search web, fetch URL content), memory-and-profile (session memory, checkpoints), navigating-terminal (explore filesystem)\n\nBehavior:\n- Start with web_search for external research\n- Use nvfs-resolve to find relevant VFS files\n- Use session_recent to check past work on the topic\n- Use fetch to read full articles from URLs\n- Synthesize findings before presenting\n- Reference the web-search-and-fetch skill for search flags", temperature: 0.5 },
+ 'security': { name: 'Security Expert', systemPrompt: "You are a WebX11 newOS4 security expert. You handle threat assessment, command control, and safety.\n\nAvailable commands: security, security-level, cmd-allow, cmd-deny, guardrail, guardrail-add, guardrail-remove, shadow, shadow-commit, shadow-rollback, health-check, check, logs, debug\n\nSkills available: security-and-safety (security levels, command blocking, safety checklist), debug-panel-and-logs (diagnostics, logging), system-maintenance (config, health checks)\n\nBehavior:\n- Always check current security level first with security\n- Use guardrail to inspect and manage security policies\n- Use shadow for safe execution of suspicious commands\n- Never recommend factory-reset without backup first\n- Use cmd-deny to block dangerous commands\n- Reference the security-and-safety skill for level descriptions", temperature: 0.2 }
};
}
registerCommands() {
@@ -16722,6 +17179,32 @@ if (typeof window !== 'undefined') {
this.activeProfile = args[0];
this.saveActiveProfile();
this.terminal.print(`Context set to: ${this.contextProfiles[args[0]].name}`, "success");
+ // Also switch agent loop profile to match
+ const PROFILE_MAP = {
+ 'coding': 'coder',
+ 'debug': 'minimal',
+ 'creative': 'creative',
+ 'technical': 'research',
+ 'chat': 'default',
+ 'system-admin': 'safe',
+ 'research': 'research',
+ 'security': 'safe'
+ };
+ const targetLoop = PROFILE_MAP[args[0]];
+ if (targetLoop) {
+ const aiMod = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiMod && aiMod._switchLoopProfile) {
+ aiMod._switchLoopProfile(targetLoop);
+ this.terminal.print(` Agent loop switched to: ${targetLoop}`, "text-muted");
+ }
+ } else {
+ // Custom profile — rebuild system prompt without loop switch
+ const aiMod = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiMod && aiMod.buildSystemPrompt) {
+ aiMod.systemPrompt = aiMod.buildSystemPrompt();
+ }
+ this.terminal.print(` No agent loop mapped for '${args[0]}'`, "text-muted");
+ }
} else {
this.terminal.print(`Profile '${args[0]}' not found`, "error");
}
@@ -16771,7 +17254,8 @@ if (typeof window !== 'undefined') {
const aiModule = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
if (aiModule) {
// Set temperature on ollamaManager modelParams temporarily
- const om = this.terminal.ollamaManager;
+ const om = this.terminal?.ollamaManager;
+ if (!om) { this.terminal?.print('⚠️ Ollama manager not available', 'error'); return; }
const savedTemp = om.modelParams ? om.modelParams.temperature : undefined;
if (om.modelParams) {
om.modelParams.temperature = temperature;
diff --git a/modules/core/module-aiassist.js b/modules/core/module-aiassist.js
index 6359f17..8a0a7c8 100644
--- a/modules/core/module-aiassist.js
+++ b/modules/core/module-aiassist.js
@@ -22,6 +22,8 @@
this._toolDefs = null;
this._toolCallLog = [];
this._debugPanelEnabled = false;
+ this._lastRawResponse = null;
+ this._lastRawMessages = null;
// 🔒 Guardrail Layer
this._guardrailPolicy = null;
this._shadowSnapshot = null;
@@ -431,7 +433,7 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'smart_context_list',
- description: 'List available AI context profiles (e.g. coding, debug, creative, technical). Returns profile names and system prompts.',
+ description: 'List available AI context profiles (e.g. coding, debug, creative, technical, chat, system-admin, research, security). Each profile sets the AI system prompt, temperature, and behavior for a specific task type. Also switches the agent loop profile (max turns, tool set) to match. Returns profile names, descriptions, and system prompts. Use this when the user task has a clear domain (coding, debugging, research, etc.) to optimize AI behavior.',
parameters: {
type: 'object',
properties: {},
@@ -442,11 +444,11 @@ buildSystemPrompt() {
type: 'function',
function: {
name: 'smart_context_set',
- description: "Set the active AI context profile. Changes the AI's behavior (temperature, style) for subsequent responses.",
+ description: "Set the active AI context profile. Changes the AI system prompt, temperature, and behavior for subsequent responses. Also switches the agent loop profile (max turns, tool set) to match. Available profiles: coding (code editing), debug (bug fixing), creative (brainstorming), technical (documentation), chat (casual), system-admin (maintenance), research (information gathering), security (threat assessment). Use this when the user task changes domain.",
parameters: {
type: 'object',
properties: {
- name: { type: 'string', description: 'Profile name (e.g. "coding", "debug", "creative", "technical", "chat")' }
+ name: { type: 'string', description: 'Profile name (e.g. "coding", "debug", "creative", "technical", "chat", "system-admin", "research", "security")' }
},
required: ['name']
}
@@ -632,6 +634,11 @@ buildSystemPrompt() {
this.commands['bench-compare'] = (args) => this.benchCompareCmd(args);
this.commands['debug'] = () => this.toggleDebugPanel();
this.commands['debug-ai'] = () => this.toggleDebug();
+ this.commands['debug-ai-last'] = () => this._showLastRawResponse();
+ this.commands['debug-ai-send'] = () => this._showLastRawRequest();
+ this.commands['debug-ai-log'] = () => this._showToolCallLog();
+ this.commands['archive-list'] = () => this._archiveList();
+ this.commands['archive-read'] = (args) => this._archiveRead(args);
this.commands['guardrail'] = (args) => this.guardrailCmd(args);
this.commands['guardrail-add'] = (args) => this.guardrailCmd(['add', ...args]);
this.commands['guardrail-remove'] = (args) => this.guardrailCmd(['remove', ...args]);
@@ -773,9 +780,14 @@ buildSystemPrompt() {
// Log the AI interaction
this.terminal.log('INFO', 'AI', 'Query: ' + userMessage.substring(0, 100));
if (loopResult && loopResult.toolTurns) {
- this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s)');
+ const toolNames = this._toolCallLog.slice(-(loopResult.toolTurns || 0)).map(function(e) { return e.name; }).join(', ');
+ this.terminal.log('INFO', 'AI', 'Used ' + loopResult.toolTurns + ' tool(s): ' + toolNames);
}
}
+ // Auto-archive every 10 messages
+ if (this._messageCount > 0 && this._messageCount % 10 === 0) {
+ this._archiveSession('Auto-archive at message ' + this._messageCount);
+ }
// Register session save on page unload (runs once per session)
if (!this._sessionSaved) {
this._sessionSaved = true;
@@ -807,12 +819,13 @@ buildSystemPrompt() {
if (!this._debugPanelEnabled) return;
const panel = document.getElementById('debug-ai-panel');
if (!panel) return;
+ const activeTab = panel.getAttribute('data-tab') || 'tools';
const recent = this._toolCallLog.slice(-20);
- let html = '';
+ let toolHtml = '';
for (const entry of recent) {
const statusClass = entry.error ? 'debug-entry-error' : 'debug-entry-ok';
const argsStr = typeof entry.args === 'string' ? entry.args.substring(0, 60) : JSON.stringify(entry.args).substring(0, 60);
- html += '<div class="debug-entry ' + statusClass + '">' +
+ toolHtml += '<div class="debug-entry ' + statusClass + '">' +
'<span class="debug-turn">#' + entry.turn + '</span> ' +
'<span class="debug-name">' + this._escapeHtml(entry.name) + '</span> ' +
'<span class="debug-args">' + this._escapeHtml(argsStr) + '</span> ' +
@@ -820,14 +833,41 @@ buildSystemPrompt() {
(entry.error ? ' <span class="debug-err">⚠️</span>' : '') +
'</div>';
}
+ // Raw I/O tab content
+ let rawHtml = '';
+ if (this._lastRawResponse) {
+ const r = this._lastRawResponse;
+ const content = (r.message && r.message.content) || '';
+ const hasTools = r.tool_calls && r.tool_calls.length > 0;
+ rawHtml += '<div style="padding:6px 8px;font-size:0.75em;line-height:1.3">';
+ if (r.error) rawHtml += '<div style="color:var(--error-color)">⚠️ ' + this._escapeHtml(r.error) + '</div>';
+ if (content) {
+ const safe = this._escapeHtml(content.length > 300 ? content.substring(0, 300) + '...' : content);
+ rawHtml += '<div><strong>Content:</strong> ' + safe + '</div>';
+ }
+ if (hasTools) {
+ rawHtml += '<div><strong>Tools:</strong> ' + r.tool_calls.map(function(tc) { return tc.function.name; }).join(', ') + '</div>';
+ }
+ if (this._lastRawMessages) {
+ rawHtml += '<div style="margin-top:4px;color:var(--text-muted)">Messages: ' + this._lastRawMessages.length + '</div>';
+ }
+ rawHtml += '</div>';
+ } else {
+ rawHtml = '<div style="padding:8px;color:var(--text-muted);font-size:0.75em">No AI response yet. Send a message first.</div>';
+ }
+ const tabBar = '<div style="display:flex;border-bottom:1px solid #333">' +
+ '<span style="cursor:pointer;padding:4px 10px;font-size:0.8em;' + (activeTab === 'tools' ? 'background:var(--bg-tertiary);border-bottom:2px solid var(--success-color);font-weight:bold' : 'color:var(--text-muted)') + '" onclick="var p=document.getElementById(\'debug-ai-panel\');p.setAttribute(\'data-tab\',\'tools\');var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m)m._updateDebugPanel()">🔧 Tools</span>' +
+ '<span style="cursor:pointer;padding:4px 10px;font-size:0.8em;' + (activeTab === 'raw' ? 'background:var(--bg-tertiary);border-bottom:2px solid var(--success-color);font-weight:bold' : 'color:var(--text-muted)') + '" onclick="var p=document.getElementById(\'debug-ai-panel\');p.setAttribute(\'data-tab\',\'raw\');var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m)m._updateDebugPanel()">📡 Raw I/O</span>' +
+ '</div>';
+ const bodyHtml = activeTab === 'raw' ? rawHtml : toolHtml;
panel.innerHTML = '<div class="debug-header" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;border-bottom:1px solid #333;font-weight:bold;color:var(--success-color);font-size:0.9em;flex-wrap:wrap;gap:4px;">' +
- '<span>🔧 Tool Log <span class="debug-count" style="color:var(--text-muted);font-weight:normal;font-size:0.85em;">' + this._toolCallLog.length + ' calls</span></span>' +
+ '<span>🔧 AI Debug <span class="debug-count" style="color:var(--text-muted);font-weight:normal;font-size:0.85em;">' + this._toolCallLog.length + ' calls</span></span>' +
'<span style="display:flex;gap:4px;">' +
'<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var t=m._toolCallLog.map(function(e){return\'#\'+e.turn+\' \'+e.name+\' (\'+e.duration+\'s)\'+(e.error?\' ⚠️\':\'\')+\'\\n\'+JSON.stringify(e.args).substring(0,80)}).join(\'\\n\\n\');navigator.clipboard.writeText(t).then(function(){m.terminal&&m.terminal.print(\'📋 Debug log copied to clipboard\',\'success\')}).catch(function(){prompt(\'Copy debug log:\',t)})}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📋</button>' +
'<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var json=JSON.stringify(m._toolCallLog,null,2);var b=new Blob([json],{type:\'application/json\'});var u=URL.createObjectURL(b);var a=document.createElement(\'a\');a.href=u;a.download=\'debug-log-\'+new Date().toISOString().slice(0,19)+\'.json\';document.body.appendChild(a);a.click();setTimeout(function(){document.body.removeChild(a);URL.revokeObjectURL(u)},2000);m.terminal&&m.terminal.print(\'📥 Debug log exported\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">📥</button>' +
'<button onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m){var existing=[];var raw=m.terminal.fs.readFile(\'/debug.log\');if(raw){try{existing=JSON.parse(raw);}catch(e){}}var log=m._toolCallLog;var seen={};existing.forEach(function(e){seen[e.turn]=true;});log.forEach(function(e){if(!seen[e.turn]){existing.push(e);seen[e.turn]=true;}});var json=JSON.stringify(existing,null,2);m.terminal.fs.writeFile(\'/debug.log\',json);m.terminal.print(\'💾 Debug log appended — \'+existing.length+\' entries total\',\'success\')}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em;min-height:24px;">💾</button>' +
'<span style="cursor:pointer;color:var(--text-muted);font-size:1.1em;touch-action:manipulation;" onclick="var p=document.getElementById(\'debug-ai-panel\');if(p)p.style.display=\'none\';var a=window.webx11&&webx11.moduleManager&&webx11.moduleManager.modules&&webx11.moduleManager.modules.WebX11AIAssistantModule;if(a)a._debugPanelEnabled=false;">✕</span>' +
- '</span></div>' + html;
+ '</span></div>' + tabBar + bodyHtml;
}
_isToolError(result) {
if (!result || typeof result !== 'string') return null;
@@ -878,6 +918,28 @@ buildSystemPrompt() {
var loopDetect = loopCfg.loopDetection || { enabled: true, threshold: 6, windowMs: 30000 };
var confEsc = loopCfg.confidenceEscalation || { enabled: true, threshold: 90 };
var finalPrompt = loopCfg.finalTurnPrompt || 'This is your last tool turn. You MUST provide a final answer after this tool call. Synthesize what you know.';
+ // Memory-augmented planning: inject prior loop lessons as negative constraints
+ try {
+ var fs = this.terminal.fs;
+ if (fs) {
+ var index = fs.fsReadJSON('/skills/index.json') || {};
+ var lessonKeys = Object.keys(index).filter(function(k) { return k.indexOf('loop-lesson-') === 0; });
+ if (lessonKeys.length > 0) {
+ var recentLessons = lessonKeys.sort().slice(-3);
+ var lessonTexts = [];
+ for (var li = 0; li < recentLessons.length; li++) {
+ var lc = fs.readFile('/skills/' + recentLessons[li] + '.md');
+ if (lc) {
+ var excerpt = lc.substring(0, 300);
+ lessonTexts.push('[Lesson: ' + recentLessons[li] + '] ' + excerpt);
+ }
+ }
+ if (lessonTexts.length > 0) {
+ messages.push({ role: 'system', content: 'Prior loop lessons (read before planning):\n' + lessonTexts.join('\n---\n') + '\n\nUse these to avoid repeating past mistakes. If lessons mention tool failures, verify those tools before calling them.' });
+ }
+ }
+ }
+ } catch (e) { /* VFS not available — skip memory-augmented planning */ }
let toolTurns = 0;
const _seenCalls = new Map();
const _loopWindow = loopDetect.windowMs || 30000;
@@ -892,6 +954,9 @@ buildSystemPrompt() {
const thinkStart = Date.now();
const res = await this.terminal.ollamaManager.chatWithTools(messages, this.getToolDefinitions());
const thinkTime = Math.round((Date.now() - thinkStart) / 1000);
+ // Store raw response for debug-ai-last
+ this._lastRawResponse = res;
+ this._lastRawMessages = messages.slice();
if (this._cancelled) return { finalAnswer: null, toolTurns };
if (res.error) { this.terminal.print(`⚠️ AI model error (${thinkTime}s): ${res.error}`, 'error'); return { finalAnswer: null, toolTurns }; }
if (res.message && !res.tool_calls) {
@@ -2068,10 +2133,26 @@ buildSystemPrompt() {
return JSON.stringify({ error: `Profile '${name}' not found`, available: Object.keys(scm.contextProfiles) });
}
scm.activeProfile = name;
+ scm.saveActiveProfile();
const profile = scm.contextProfiles[name];
// Rebuild system prompt to reflect new context profile
this.systemPrompt = this.buildSystemPrompt();
- return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature });
+ // Also switch agent loop profile to match
+ const PROFILE_MAP = {
+ 'coding': 'coder',
+ 'debug': 'minimal',
+ 'creative': 'creative',
+ 'technical': 'research',
+ 'chat': 'default',
+ 'system-admin': 'safe',
+ 'research': 'research',
+ 'security': 'safe'
+ };
+ const targetLoop = PROFILE_MAP[name];
+ if (targetLoop && this._switchLoopProfile) {
+ this._switchLoopProfile(targetLoop);
+ }
+ return JSON.stringify({ success: true, name, displayName: profile.name, systemPrompt: profile.systemPrompt, temperature: profile.temperature, agentLoop: targetLoop || null });
}
case 'create_mindmap': {
const mindmap = this.terminal.moduleManager.modules['MindmapModule'];
@@ -2796,6 +2877,121 @@ buildSystemPrompt() {
}
return total;
}
+ _showToolCallLog() {
+ if (!this._toolCallLog || this._toolCallLog.length === 0) {
+ this.terminal.print('No tool calls recorded this session.', 'warning');
+ return;
+ }
+ let out = '── Tool Call Log (' + this._toolCallLog.length + ' calls) ──\n';
+ for (var i = 0; i < this._toolCallLog.length; i++) {
+ var e = this._toolCallLog[i];
+ var err = e.error ? ' ⚠️' : '';
+ var argsStr = typeof e.args === 'string' ? e.args.substring(0, 80) : JSON.stringify(e.args).substring(0, 80);
+ out += ' #' + e.turn + ' ' + e.name + '(' + argsStr + ') ' + e.duration + 's' + err + '\n';
+ }
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ }
+ _archiveList() {
+ try {
+ const fs = this.terminal.fs;
+ const files = fs.ls('/memory/archive') || [];
+ const jsonFiles = files.filter(function(f) { return f.name && f.name.endsWith('.json'); });
+ if (jsonFiles.length === 0) {
+ this.terminal.print('No archives found. Run ai-compress to create one.', 'warning');
+ return;
+ }
+ jsonFiles.sort(function(a, b) { return (a.name || '') < (b.name || '') ? 1 : -1; });
+ let out = '── Archives (' + jsonFiles.length + ') ──\n';
+ for (var i = 0; i < Math.min(jsonFiles.length, 30); i++) {
+ var f = jsonFiles[i];
+ var size = f.size ? '(' + f.size + 'b)' : '';
+ out += ' ' + f.name + ' ' + size + '\n';
+ }
+ out += '── End ──\nUse: archive-read <filename>';
+ this.terminal.print(out, 'info');
+ } catch (e) {
+ this.terminal.print('Error listing archives: ' + e.message, 'error');
+ }
+ }
+ _archiveRead(args) {
+ var name = (args && args[0]) || '';
+ if (!name) {
+ this.terminal.print('Usage: archive-read <filename> (e.g. 2026-06-25-14-30.json)', 'warning');
+ return;
+ }
+ try {
+ const fs = this.terminal.fs;
+ var data = fs.fsReadJSON('/memory/archive/' + name);
+ if (!data) {
+ this.terminal.print('Archive not found: ' + name, 'error');
+ return;
+ }
+ var out = '── Archive: ' + name + ' ──\n';
+ out += 'Archived: ' + (data.archivedAt || '?') + '\n';
+ out += 'Summary: ' + (data.summary || '(none)') + '\n';
+ out += 'User messages: ' + (data.user?.messageCount || 0) + '\n';
+ out += 'System: ' + (data.system?.modules || 0) + ' modules, ' + (data.system?.model || '?') + '\n';
+ out += 'Context: ' + (data.context?.messageCount || 0) + ' msgs, ~' + (data.context?.estimatedTokens || 0) + ' tokens\n';
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ } catch (e) {
+ this.terminal.print('Error reading archive: ' + e.message, 'error');
+ }
+ }
+ _showLastRawResponse() {
+ if (!this._lastRawResponse) {
+ this.terminal.print('No AI response recorded yet. Send a message first.', 'warning');
+ return;
+ }
+ const r = this._lastRawResponse;
+ var content = (r.message && r.message.content) || '';
+ var hasTools = r.tool_calls && r.tool_calls.length > 0;
+ var toolStr = hasTools ? r.tool_calls.map(function(tc) { return tc.function.name + '(' + (tc.function.arguments || '{}') + ')'; }).join(', ') : '';
+ // Render as HTML card for better readability
+ var html = '<div class="msg-card ai" style="border:1px solid var(--border-color);border-radius:6px;margin:4px 0;overflow:hidden">' +
+ '<div class="msg-card-header" style="display:flex;justify-content:space-between;align-items:center;padding:6px 10px;background:var(--bg-secondary);border-bottom:1px solid var(--border-color);font-weight:bold;font-size:0.9em">' +
+ '<span>🤖 Last AI Response</span>' +
+ '<span style="display:flex;gap:4px">' +
+ '<button class="card-btn" onclick="var m=webx11.moduleManager.modules.WebX11AIAssistantModule;if(m&&m._lastRawResponse){navigator.clipboard.writeText(JSON.stringify(m._lastRawResponse,null,2)).then(function(){m.terminal.print(\'📋 Raw response copied\',\'success\')})}" style="background:var(--bg-tertiary);border:1px solid var(--border-color);color:var(--text-color);padding:2px 6px;border-radius:3px;cursor:pointer;font-size:0.75em">📋 Copy Raw</button>' +
+ '</span></div>' +
+ '<div class="msg-card-body" style="padding:8px 10px;font-size:0.85em;line-height:1.4">';
+ if (r.error) {
+ html += '<div style="color:var(--error-color)">⚠️ Error: ' + this._escapeHtml(r.error) + '</div>';
+ }
+ if (content) {
+ var safe = this._escapeHtml(content.length > 1000 ? content.substring(0, 1000) + '...(' + content.length + ' chars)' : content);
+ html += '<div style="margin-bottom:4px"><strong>Content:</strong><br>' + safe + '</div>';
+ } else if (!hasTools) {
+ html += '<div style="color:var(--warning-color)">⚠️ Empty response (no content, no tool calls)</div>';
+ }
+ if (hasTools) {
+ html += '<div style="margin-top:4px"><strong>Tool calls:</strong> ' + this._escapeHtml(toolStr) + '</div>';
+ }
+ html += '</div></div>';
+ this.terminal.printHTML(html);
+ }
+ _showLastRawRequest() {
+ if (!this._lastRawMessages) {
+ this.terminal.print('No AI request recorded yet. Send a message first.', 'warning');
+ return;
+ }
+ const msgs = this._lastRawMessages;
+ let out = '── Last AI Request (' + msgs.length + ' messages) ──\n';
+ for (let i = 0; i < msgs.length; i++) {
+ const m = msgs[i];
+ const role = m.role || '?';
+ const content = (m.content || '').substring(0, 200);
+ const suffix = (m.content || '').length > 200 ? '...' : '';
+ out += ' [' + i + '] ' + role + ': ' + content + suffix + '\n';
+ if (i > 8 && i < msgs.length - 2) {
+ out += ' ... (' + (msgs.length - i - 2) + ' more messages) ...\n';
+ i = msgs.length - 3;
+ }
+ }
+ out += '── End ──';
+ this.terminal.print(out, 'info');
+ }
async _compressContext() {
if (this.context.length <= 6) return;
const totalTokens = this._estimateContextTokens();
@@ -2821,10 +3017,58 @@ buildSystemPrompt() {
} catch (e) { /* fallback to truncation */ }
this.context = [{ role: 'system', content: compressed }, ...recentMsgs];
this.terminal.print('📦 Context compressed (' + oldMsgs.length + ' messages → 1 summary, ~' + totalTokens + ' tokens)', 'info');
+ // Also archive session snapshot
+ this._archiveSession(compressed);
+ }
+ _archiveSession(summary) {
+ try {
+ const fs = this.terminal.fs;
+ fs.mkdirRecursive('/memory/archive');
+ const now = new Date();
+ const pad = function(n) { return String(n).padStart(2, '0'); };
+ const ts = now.getFullYear() + '-' + pad(now.getMonth()+1) + '-' + pad(now.getDate()) + '-' + pad(now.getHours()) + '-' + pad(now.getMinutes());
+ const archive = {
+ archivedAt: now.toISOString(),
+ summary: summary || '',
+ user: {
+ profile: this._userProfile || {},
+ topics: [],
+ messageCount: this._messageCount
+ },
+ system: {
+ modules: this.terminal?.moduleManager?.modules ? Object.keys(this.terminal.moduleManager.modules).length : 0,
+ model: this.terminal?.ollamaManager?.currentModel || 'unknown',
+ provider: this.terminal?.ollamaManager?.currentProvider?.name || 'unknown'
+ },
+ context: {
+ messageCount: this.context.length,
+ estimatedTokens: this._estimateContextTokens()
+ }
+ };
+ fs.fsWriteJSON('/memory/archive/' + ts + '.json', archive);
+ // Rotation: keep last 30
+ try {
+ const files = fs.ls('/memory/archive') || [];
+ const jsonFiles = files.filter(function(f) { return f.name && f.name.endsWith('.json'); });
+ if (jsonFiles.length > 30) {
+ jsonFiles.sort(function(a, b) { return (a.name || '') < (b.name || '') ? -1 : 1; });
+ const toDelete = jsonFiles.slice(0, jsonFiles.length - 30);
+ for (var i = 0; i < toDelete.length; i++) {
+ fs.deleteFile('/memory/archive/' + toDelete[i].name);
+ }
+ }
+ } catch (e) { /* rotation best-effort */ }
+ } catch (e) { /* archive best-effort */ }
}
_manualCompress() {
this._compressContext().then(() => {
- this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens', 'info');
+ const fs = this.terminal.fs;
+ let archiveCount = 0;
+ try {
+ const files = fs.ls('/memory/archive') || [];
+ archiveCount = files.filter(function(f) { return f.name && f.name.endsWith('.json'); }).length;
+ } catch (e) {}
+ this.terminal.print('Context: ' + this.context.length + ' message(s), ~' + this._estimateContextTokens() + ' estimated tokens, ' + archiveCount + ' archive(s)', 'info');
}).catch(e => {
this.terminal.print('Compression error: ' + e.message, 'error');
});
@@ -3150,11 +3394,38 @@ buildSystemPrompt() {
}
if (anomalies.length === 0) return;
var suggestions = [];
+ var profileAdjusted = false;
for (var ai = 0; ai < anomalies.length; ai++) {
var a = anomalies[ai];
- if (a.type === 'max_turns_near_limit') suggestions.push('This task used ' + result.toolTurns + '/' + maxT + ' turns — consider increasing max-turns or splitting the task');
+ if (a.type === 'max_turns_near_limit') {
+ suggestions.push('This task used ' + result.toolTurns + '/' + maxT + ' turns — consider increasing max-turns or splitting the task');
+ // Auto-correct: bump maxTurns by +2
+ if (this._activeLoopProfile && this._activeLoopProfile.loop) {
+ var newMax = (this._activeLoopProfile.loop.maxTurns || this.MAX_TOOL_TURNS) + 2;
+ this._activeLoopProfile.loop.maxTurns = newMax;
+ this._saveLoopProfile(this._activeLoopProfile);
+ suggestions.push(' → Auto-adjusted maxTurns to ' + newMax);
+ profileAdjusted = true;
+ }
+ }
else if (a.type === 'high_token_usage') suggestions.push('High token usage (' + (a.value/1000).toFixed(0) + 'K) — consider a minimal-tools profile for simpler tasks');
- else if (a.type === 'tool_failures') suggestions.push(a.count + ' tool failures (' + a.tools.join(', ') + ') — check if modules are loaded or paths correct');
+ else if (a.type === 'tool_failures') {
+ suggestions.push(a.count + ' tool failures (' + a.tools.join(', ') + ') — check if modules are loaded or paths correct');
+ // Auto-correct: inject negative constraint into profile append
+ if (this._activeLoopProfile && this._activeLoopProfile.systemPrompt) {
+ var constraint = '[Constraint] Avoid tools that failed: ' + a.tools.join(', ') + '. Verify module state before calling these tools.';
+ if (this._activeLoopProfile.systemPrompt.append) {
+ if (this._activeLoopProfile.systemPrompt.append.indexOf(constraint) === -1) {
+ this._activeLoopProfile.systemPrompt.append += '\n' + constraint;
+ }
+ } else {
+ this._activeLoopProfile.systemPrompt.append = constraint;
+ }
+ this._saveLoopProfile(this._activeLoopProfile);
+ suggestions.push(' → Added constraint to profile: avoid ' + a.tools.join(', '));
+ profileAdjusted = true;
+ }
+ }
else if (a.type === 'above_benchmark') suggestions.push('This run used ' + a.ratio + 'x more tokens than benchmark avg (' + a.benchmark + ') — consider profile optimization');
}
var suggestion = suggestions.join('\n');
@@ -3162,15 +3433,47 @@ buildSystemPrompt() {
var fs = this.terminal.fs;
var index = fs.fsReadJSON('/skills/index.json') || {};
var autoSkills = Object.entries(index).filter(function(e) { return (e[1].tags || []).indexOf('auto-improvement') !== -1; });
- if (autoSkills.length < 10) {
+ // One-time cleanup: remove all existing loop-lesson files on first run
+ if (!this._loopLessonCleaned) {
+ this._loopLessonCleaned = true;
+ autoSkills.forEach(function(e) {
+ try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {}
+ delete index[e[0]];
+ });
+ autoSkills = [];
+ }
+ // Age limit: remove lessons older than 24h
+ var now = Date.now();
+ autoSkills = autoSkills.filter(function(e) {
+ var created = new Date(e[1].created).getTime();
+ if (now - created > 86400000) {
+ try { fs.unlink('/skills/' + e[0] + '.md'); } catch (ex) {}
+ delete index[e[0]];
+ return false;
+ }
+ return true;
+ });
+ // Dedup: skip if latest lesson has same anomaly types
+ var anomalyTypes = anomalies.map(function(a) { return a.type; }).sort().join(',');
+ var isDuplicate = autoSkills.length > 0 && autoSkills[autoSkills.length - 1][1].anomalyTypes === anomalyTypes;
+ // FIFO rotation: if at cap, delete oldest before creating
+ if (!isDuplicate && autoSkills.length >= 3) {
+ var oldest = autoSkills[0];
+ try { fs.unlink('/skills/' + oldest[0] + '.md'); } catch (ex) {}
+ delete index[oldest[0]];
+ autoSkills.shift();
+ }
+ if (!isDuplicate) {
var skillName = 'loop-lesson-' + Date.now();
var content = '---\nname: ' + skillName + '\ndescription: Auto-saved from agent loop anomaly detection\ntags: [auto-improvement, agent-loop, auto]\ncategory: system\n---\n## Anomalies Detected\n\n' + JSON.stringify(anomalies, null, 2) + '\n\n## Suggestions\n\n' + suggestion + '\n\n## Context\n- Profile: ' + (this._activeLoopProfile ? this._activeLoopProfile.name : 'none') + '\n- Turns: ' + result.toolTurns + '\n- Tools used: ' + toolLog.map(function(t) { return t.name; }).join(', ');
fs.mkdirRecursive('/skills');
fs.writeFile('/skills/' + skillName + '.md', content);
- index[skillName] = { name: skillName, description: 'Auto-saved loop lesson', tags: ['auto-improvement', 'agent-loop', 'auto'], category: 'system', created: new Date().toISOString(), updated: new Date().toISOString() };
+ index[skillName] = { name: skillName, description: 'Auto-saved loop lesson', tags: ['auto-improvement', 'agent-loop', 'auto'], category: 'system', created: new Date().toISOString(), updated: new Date().toISOString(), anomalyTypes: anomalyTypes };
fs.fsWriteJSON('/skills/index.json', index);
}
} catch (e) { /* VFS not available */ }
- this.terminal.print('💡 Self-improvement: ' + suggestion, 'info');
+ var msg = '💡 Self-improvement: ' + anomalies.length + ' anomaly(ies)';
+ if (profileAdjusted) msg += ' — profile auto-adjusted';
+ this.terminal.print(msg, 'info');
}
}
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index f024d99..7bb7c89 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = '2026-06-24-v4.5.80';
+ const LATEST_VER = '2026-06-30-v4.5.83';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
@@ -53,7 +53,7 @@
setSplash('Initializing terminal (3/3)', total, total, '');
// webx11 already created synchronously above — just init ext modules
if (typeof webx11._loadExtModules === 'function') {
- let extKeys = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console', 'media'];
+ let extKeys = ['calc', 'voice', 'debug-panel', 'cron', 'theme', 'wpm', 'mindmap', 'webdav', 'skill-pack-core', 'cryptogzip', 'tutorial', 'floating', 'console', 'media', 'three'];
const saved = localStorage.getItem('webx11_startup_modules');
let selected = null;
try { if (saved) selected = JSON.parse(saved); } catch (e) {}
diff --git a/modules/core/module-manager.js b/modules/core/module-manager.js
index 8cecc4d..4723bff 100644
--- a/modules/core/module-manager.js
+++ b/modules/core/module-manager.js
@@ -36,6 +36,11 @@
}
async loadModuleFromURL(url) {
try {
+ // Security: block cross-origin URLs for module loading
+ if (url && url.match(/^https?:\/\//) && !url.startsWith(window.location.origin)) {
+ this.terminal.print(`⚠️ Security: only same-origin URLs allowed for module loading`, 'error');
+ return false;
+ }
this.terminal.print(`Loading module from ${url}...`, 'info');
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
diff --git a/modules/core/module-modmgr.js b/modules/core/module-modmgr.js
index ec8c384..8c39e04 100644
--- a/modules/core/module-modmgr.js
+++ b/modules/core/module-modmgr.js
@@ -338,22 +338,27 @@
this.terminal.print(`✅ Module '${name}' marked as unloaded (commands will not be available)`, 'success');
};
- // module-reload <name> — unload + reload with diff
- this.commands['module-reload'] = (args) => {
+ // module-reload <name> — re-fetch, re-parse, re-register
+ this.commands['module-reload'] = async (args) => {
if (!args[0]) return this.terminal.print('Usage: module-reload <name>', 'warning');
const name = args[0];
const mod = this._getMod(name);
if (!mod) return this.terminal.print(`Module not found: ${name}`, 'error');
- const wasLoaded = mod.loaded !== false;
- const wasLazy = mod.lazy;
- this.terminal.print(`🔄 Reloading '${name}' (was: ${wasLoaded ? 'loaded' : 'unloaded'}, lazy=${wasLazy})`, 'info');
- mod.loaded = false;
- setTimeout(() => {
- mod.loaded = true;
- this.terminal.print(`✅ Module '${name}' reloaded`, 'success');
- const cmdCount = mod.commands ? Object.keys(mod.commands).length : 0;
- this.terminal.print(` Now: loaded=true, lazy=${mod.lazy}, ${cmdCount} commands`, 'info');
- }, 100);
+ const mm = this.terminal.moduleManager;
+ // Find the module's URL from registry
+ const regEntry = mm.moduleRegistry.find(m => m.name === name);
+ const url = regEntry ? regEntry.url : null;
+ if (!url) return this.terminal.print(`Module '${name}' has no URL — cannot reload (built-in)`, 'warning');
+ this.terminal.print(`🔄 Reloading '${name}' from ${url}...`, 'info');
+ // Unregister old instance
+ mm.unregisterModule(name);
+ // Re-fetch and re-register
+ const success = await mm.loadModuleFromURL(url);
+ if (success) {
+ this.terminal.print(`✅ Module '${name}' reloaded successfully`, 'success');
+ } else {
+ this.terminal.print(`❌ Failed to reload '${name}'`, 'error');
+ }
};
// module-reset <name> — clear module state and reload
diff --git a/modules/core/module-smartctx.js b/modules/core/module-smartctx.js
index 81929f8..0bfc91a 100644
--- a/modules/core/module-smartctx.js
+++ b/modules/core/module-smartctx.js
@@ -40,11 +40,14 @@
}
getDefaultProfiles() {
return {
- 'coding': { name: 'Coding Assistant', systemPrompt: "You are an expert programmer.", temperature: 0.7 },
- 'debug': { name: 'Debug Helper', systemPrompt: "You are a debugging expert.", temperature: 0.5 },
- 'creative': { name: 'Creative Writer', systemPrompt: "You are a creative writer.", temperature: 0.9 },
- 'technical': { name: 'Technical Docs', systemPrompt: "You are a technical writer.", temperature: 0.3 },
- 'chat': { name: 'Casual Chat', systemPrompt: "You are a friendly assistant.", temperature: 0.8 }
+ 'coding': { name: 'Coding Assistant', systemPrompt: "You are a WebX11 newOS4 coding expert. You have access to the full terminal, VFS, and module system.\n\nAvailable commands: view_module, edit_module, test_module, reload_module, list_modified_modules, check_updates, load-local, loadmod, removemod, startup-modules, node, js-run, git, diff, patch\n\nSkills available: module-management (view/edit/test/reload modules), skills-system-guide (create/patch skills)\n\nBehavior:\n- Always show exact code changes when editing modules\n- Use edit_module for modifications, test_module to verify\n- Create checkpoints before risky edits: checkpoint mod-edit-<name>\n- Reference the module-management skill for the full workflow\n- When the user reports a bug, first view_module to inspect, then edit\n- Use nvfs_resolve to find relevant files before editing\n- Prefer write_file for creating new files, edit_module for modifying modules", temperature: 0.7 },
+ 'debug': { name: 'Debug Helper', systemPrompt: "You are a WebX11 newOS4 debugging expert. Your goal is to diagnose and fix issues systematically.\n\nAvailable commands: debug, debug-ai, debug-ai-last, debug-ai-send, debug-ai-log, logs, logrotate, health-check, check, mem, status, archive-list, archive-read, guardrail, shadow, shadow-commit, shadow-rollback\n\nSkills available: debug-panel-and-logs (toggle debug panel, session logging, diagnostics), system-maintenance (health checks, config, security)\n\nBehavior:\n- Start with health-check and check to assess system state\n- Use debug-ai to see the last AI tool call and response\n- Use logs 50 ERROR to find error entries\n- Check browser console for JS errors\n- Use guardrail to inspect security policies\n- Use shadow for safe execution of risky commands\n- Always check module loading status first", temperature: 0.3 },
+ 'creative': { name: 'Creative Writer', systemPrompt: "You are a creative assistant for WebX11 newOS4. You help with brainstorming, content creation, and design.\n\nAvailable commands: mindmap new/add/show/save/load/export, floating-add/remove/list, ai-system, ai-config\n\nSkills available: mindmap-workflow (create/view/edit mindmaps), floating-ui (create floating buttons/menus/panels)\n\nBehavior:\n- Use mindmap for brainstorming and organizing ideas\n- Use floating-add to create quick-access UI elements\n- Use web_search and web_fetch for research and inspiration\n- Higher temperature (0.9) for creative divergence\n- When the user says \"brainstorm\" or \"diagram\", create a mindmap\n- Use create_mindmap AI tool for visual architecture diagrams\n- Reference the mindmap-workflow skill for full capabilities", temperature: 0.9 },
+ 'technical': { name: 'Technical Docs', systemPrompt: "You are a technical documentation expert for WebX11 newOS4. You help with docs, guides, and system understanding.\n\nAvailable commands: help, commands, status, modules, health-check, nvfs-index, nvfs-resolve, view_module, cat, edit, grep, find, tree\n\nSkills available: navigating-terminal (explore terminal, filesystem, system state), web-search-and-fetch (search web, fetch URLs), skills-system-guide (create/patch skills)\n\nBehavior:\n- Use help and commands to discover available functionality\n- Use nvfs-resolve to find files by concept\n- Use view_module to inspect module source for documentation\n- When creating documentation, use write_file to save to VFS\n- Reference the navigating-terminal skill for filesystem exploration\n- Use web_search for external research", temperature: 0.3 },
+ 'chat': { name: 'Casual Chat', systemPrompt: "You are a friendly, helpful assistant for WebX11 newOS4. You handle general questions, casual conversation, and light tasks.\n\nAvailable commands: ai, ask, search, fetch, status, help, user_get, user_set, session_save, session_recent, my-set, my-get\n\nSkills available: memory-and-profile (user profile, session memory, checkpoints), web-search-and-fetch (search web, fetch URLs)\n\nBehavior:\n- Be conversational and approachable\n- Use user_get/user_set to remember user preferences\n- Use session_recent at start to recall past work\n- Use session_save after completing tasks\n- For complex tasks, suggest switching to a more specific context profile\n- Reference the memory-and-profile skill for persistence features", temperature: 0.8 },
+ 'system-admin': { name: 'System Admin', systemPrompt: "You are a WebX11 newOS4 system administrator. You handle maintenance, backup, security, and configuration.\n\nAvailable commands: config-list, config-set, config-delete, config-reset, security-level, security, cmd-allow, cmd-deny, backup, vfs-export, webdav-backup, webdav-restore, webdav-config, webdav-status, factory-reset, reset-vfs, checkpoint, checkpoint-list, rollback, health-check, check_updates, font-size\n\nSkills available: system-maintenance (reset, config, security, health checks), backup-and-restore (VFS backup, WebDAV cloud backup, restore), webdav-operations (cloud storage, file sync), security-and-safety (security levels, command blocking)\n\nBehavior:\n- Always create a checkpoint before making system changes\n- Use webdav-backup before destructive operations\n- Check security-level before running commands\n- Use health-check for full system audit\n- Reference the system-maintenance skill for reset procedures\n- Reference the backup-and-restore skill for data safety", temperature: 0.3 },
+ 'research': { name: 'Research Assistant', systemPrompt: "You are a research assistant for WebX11 newOS4. You gather information, analyze data, and synthesize findings.\n\nAvailable commands: search, fetch, web_search, web_fetch, nvfs-resolve, nvfs-index, grep, find, cat, view_module, session_recent, session_load, session_stats\n\nSkills available: web-search-and-fetch (search web, fetch URL content), memory-and-profile (session memory, checkpoints), navigating-terminal (explore filesystem)\n\nBehavior:\n- Start with web_search for external research\n- Use nvfs-resolve to find relevant VFS files\n- Use session_recent to check past work on the topic\n- Use fetch to read full articles from URLs\n- Synthesize findings before presenting\n- Reference the web-search-and-fetch skill for search flags", temperature: 0.5 },
+ 'security': { name: 'Security Expert', systemPrompt: "You are a WebX11 newOS4 security expert. You handle threat assessment, command control, and safety.\n\nAvailable commands: security, security-level, cmd-allow, cmd-deny, guardrail, guardrail-add, guardrail-remove, shadow, shadow-commit, shadow-rollback, health-check, check, logs, debug\n\nSkills available: security-and-safety (security levels, command blocking, safety checklist), debug-panel-and-logs (diagnostics, logging), system-maintenance (config, health checks)\n\nBehavior:\n- Always check current security level first with security\n- Use guardrail to inspect and manage security policies\n- Use shadow for safe execution of suspicious commands\n- Never recommend factory-reset without backup first\n- Use cmd-deny to block dangerous commands\n- Reference the security-and-safety skill for level descriptions", temperature: 0.2 }
};
}
registerCommands() {
@@ -62,6 +65,32 @@
this.activeProfile = args[0];
this.saveActiveProfile();
this.terminal.print(`Context set to: ${this.contextProfiles[args[0]].name}`, "success");
+ // Also switch agent loop profile to match
+ const PROFILE_MAP = {
+ 'coding': 'coder',
+ 'debug': 'minimal',
+ 'creative': 'creative',
+ 'technical': 'research',
+ 'chat': 'default',
+ 'system-admin': 'safe',
+ 'research': 'research',
+ 'security': 'safe'
+ };
+ const targetLoop = PROFILE_MAP[args[0]];
+ if (targetLoop) {
+ const aiMod = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiMod && aiMod._switchLoopProfile) {
+ aiMod._switchLoopProfile(targetLoop);
+ this.terminal.print(` Agent loop switched to: ${targetLoop}`, "text-muted");
+ }
+ } else {
+ // Custom profile — rebuild system prompt without loop switch
+ const aiMod = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
+ if (aiMod && aiMod.buildSystemPrompt) {
+ aiMod.systemPrompt = aiMod.buildSystemPrompt();
+ }
+ this.terminal.print(` No agent loop mapped for '${args[0]}'`, "text-muted");
+ }
} else {
this.terminal.print(`Profile '${args[0]}' not found`, "error");
}
@@ -111,7 +140,8 @@
const aiModule = this.terminal?.moduleManager?.modules?.WebX11AIAssistantModule;
if (aiModule) {
// Set temperature on ollamaManager modelParams temporarily
- const om = this.terminal.ollamaManager;
+ const om = this.terminal?.ollamaManager;
+ if (!om) { this.terminal?.print('⚠️ Ollama manager not available', 'error'); return; }
const savedTemp = om.modelParams ? om.modelParams.temperature : undefined;
if (om.modelParams) {
om.modelParams.temperature = temperature;
diff --git a/modules/ext/mindmap.js b/modules/ext/mindmap.js
index a5e7286..acce9aa 100644
--- a/modules/ext/mindmap.js
+++ b/modules/ext/mindmap.js
@@ -1075,6 +1075,61 @@ class MindmapModule {
}
}
}
+ _svgFullscreen(args) {
+ const cur = this._ensureMindmap();
+ if (!cur) return this.terminal.print('No mindmap. Create one with mindmap new first.', 'warning');
+ const name = args[0] || this.current || 'mindmap';
+ const layout = this._layoutTree(cur.root);
+ const colorMap = {
+ red: '#fb7185', green: '#34d399', yellow: '#fbbf24', blue: '#818cf8',
+ cyan: '#22d3ee', magenta: '#e879f9', white: '#e0e0e0'
+ };
+ const xShift = layout.minX === Infinity ? 0 : -layout.minX;
+ const svgWidth = layout.width;
+ const svgHeight = layout.height;
+ let svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + svgWidth + ' ' + svgHeight + '" style="background:#0a0a14;font-family:monospace;">';
+ for (const e of layout.edges) {
+ const sx = e.x1 + xShift + 30, sy = e.y1;
+ const ex = e.x2 + xShift + 30, ey = e.y2;
+ const mx = (sx + ex) / 2;
+ svg += '<path d="M' + sx + ',' + sy + ' C' + mx + ',' + (sy + 20) + ' ' + mx + ',' + (ey - 20) + ' ' + ex + ',' + ey + '" fill="none" stroke="#2a2a4a" stroke-width="1.5"/>';
+ }
+ for (const n of layout.nodes) {
+ const nx = n.x + xShift + 30;
+ const color = colorMap[n.color] || '#818cf8';
+ const fontSize = n.w < 120 ? '9' : '10';
+ const text = n.text.length > 18 ? n.text.substring(0, 16) + '..' : n.text;
+ svg += '<rect x="' + nx + '" y="' + n.y + '" width="' + n.w + '" height="' + n.h + '" rx="6" fill="' + color + '18" stroke="' + color + '" stroke-width="1.5"/>';
+ svg += '<text x="' + (nx + n.w / 2) + '" y="' + (n.y + n.h / 2 + 4) + '" text-anchor="middle" fill="#e0e0e0" font-size="' + fontSize + '" font-family="monospace">' + this._escapeHtml(text) + '</text>';
+ }
+ svg += '</svg>';
+
+ // Create fullscreen overlay
+ const overlay = document.createElement('div');
+ overlay.id = 'mindmap-svg-overlay';
+ overlay.style.cssText = 'position:fixed;inset:0;z-index:9999;background:#0a0a14;display:flex;align-items:center;justify-content:center;overflow:auto';
+ overlay.innerHTML = svg;
+ document.body.appendChild(overlay);
+
+ // Close button
+ const closeBtn = document.createElement('button');
+ closeBtn.textContent = '✕';
+ closeBtn.style.cssText = 'position:fixed;top:12px;right:12px;z-index:10000;background:rgba(0,0,0,0.6);color:#e0e0e0;border:1px solid #444;border-radius:6px;padding:6px 14px;font-size:16px;cursor:pointer;font-family:monospace';
+ closeBtn.addEventListener('click', () => { overlay.remove(); closeBtn.remove(); });
+ document.body.appendChild(closeBtn);
+
+ // Escape key
+ const escHandler = (e) => {
+ if (e.key === 'Escape') {
+ overlay.remove();
+ closeBtn.remove();
+ document.removeEventListener('keydown', escHandler);
+ }
+ };
+ document.addEventListener('keydown', escHandler);
+
+ this.terminal.print(`🖼️ SVG fullscreen: ${layout.nodes.length} nodes, ${svgWidth}x${svgHeight}`, 'success');
+ }
// ── Main dispatcher ──
registerCommands() {
this.commands['mindmap'] = (args) => {
@@ -1116,7 +1171,15 @@ class MindmapModule {
case 'clone': return this._clone(rest);
case 'archive': return this._archive(rest);
case 'refresh-html': return this._refreshHtml(rest);
- case 'svg': return this._svg(rest);
+ case 'svg': {
+ const fsIdx = rest.indexOf('--fullscreen');
+ if (fsIdx >= 0) {
+ const clean = rest.filter(a => a !== '--fullscreen');
+ return this._svgFullscreen(clean);
+ }
+ return this._svg(rest);
+ }
+ case 'svg-fullscreen': return this._svgFullscreen(rest);
default: this.terminal.print('Unknown subcommand: ' + sub + '. Try: mindmap help', 'warning');
}
};
diff --git a/modules/ext/three.js b/modules/ext/three.js
new file mode 100644
index 0000000..d72afa1
--- /dev/null
+++ b/modules/ext/three.js
@@ -0,0 +1,1176 @@
+// newOS4 Three.js 3D Module (v4.5.82)
+// Commands: three <subcommand> [...]
+// Two modes: top panel (default) + fullscreen with terminal overlay
+// Lazy-loads Three.js from /downloads/ on first use.
+
+class ThreeModule {
+ constructor() {
+ this.commands = {};
+ this.commandsMeta = {
+ 'three': { argType: 'enum', enumFn: '_listSubcommands' },
+ };
+ this.terminal = null;
+ this._loaded = false;
+ this._scene = null;
+ this._camera = null;
+ this._renderer = null;
+ this._controls = null;
+ this._objects = [];
+ this._animId = null;
+ this._container = null;
+ this._closeBtn = null;
+ this._idCounter = 0;
+ this._orbitEnabled = true;
+ this._backgroundColor = 0x0a0a14;
+ this._THREE = null;
+ this._mode = 'top'; // 'top' or 'full'
+ this._visible = false;
+ this._savedTerminalStyles = null;
+ this._savedInputStyles = null;
+ this._savedHeaderStyles = null;
+ this._escHandler = null;
+ }
+
+ requirements() {
+ const hasWebGL = (() => {
+ try {
+ const c = document.createElement('canvas');
+ return !!(c.getContext('webgl') || c.getContext('experimental-webgl'));
+ } catch (e) { return false; }
+ })();
+ if (!hasWebGL) {
+ return { met: false, missing: ['WebGL not supported in this browser'] };
+ }
+ return { met: true, missing: [] };
+ }
+
+ getHelpInfo() {
+ return {
+ 'three': 'three <subcommand> [...] — 3D scene builder (Three.js). Subcommands: help, scene, fullscreen, hide, mode, add, camera, orbit, clear, list, remove, mindmap, export, preset, bgcolor. Run `three help` for full list.'
+ };
+ }
+
+ _listSubcommands() {
+ return ['help', 'scene', 'fullscreen', 'hide', 'mode', 'add', 'camera', 'orbit', 'clear', 'list', 'remove', 'mindmap', 'export', 'preset', 'bgcolor'];
+ }
+
+ registerCommands() {
+ this.commands['three'] = (args) => this._dispatch(args);
+ }
+
+ // ── Lazy load Three.js ──
+
+ async _ensureLoaded() {
+ if (this._loaded) return true;
+ if (typeof THREE !== 'undefined') {
+ this._THREE = THREE;
+ this._loaded = true;
+ return true;
+ }
+ this.terminal.print('📦 Loading Three.js (590KB)...', 'info');
+ try {
+ await this._loadScript('downloads/three.min.js');
+ await this._loadScript('downloads/OrbitControls.js');
+ this._THREE = THREE;
+ this._loaded = true;
+ this.terminal.print('✅ Three.js loaded', 'success');
+ return true;
+ } catch (e) {
+ this.terminal.print('❌ Failed to load Three.js: ' + e.message, 'error');
+ return false;
+ }
+ }
+
+ _loadScript(url) {
+ return new Promise((resolve, reject) => {
+ const s = document.createElement('script');
+ s.src = url;
+ s.onload = () => { s.remove(); resolve(); };
+ s.onerror = () => { s.remove(); reject(new Error('Failed to load ' + url)); };
+ document.head.appendChild(s);
+ });
+ }
+
+ // ── Container management ──
+
+ _createContainer() {
+ if (this._container) return;
+
+ // Main container
+ this._container = document.createElement('div');
+ this._container.id = 'three-canvas-container';
+ this._container.style.cssText = `
+ position: fixed;
+ top: 0; left: 0;
+ width: 100vw;
+ z-index: 400;
+ pointer-events: none;
+ display: none;
+ `;
+ document.body.appendChild(this._container);
+
+ // Close button
+ this._closeBtn = document.createElement('button');
+ this._closeBtn.id = 'three-close-btn';
+ this._closeBtn.textContent = '✕';
+ this._closeBtn.title = 'Close 3D view (Esc)';
+ this._closeBtn.style.cssText = `
+ position: fixed;
+ top: 12px; right: 12px;
+ z-index: 9999;
+ background: rgba(0,0,0,0.6);
+ color: #e0e0e0;
+ border: 1px solid #444;
+ border-radius: 6px;
+ padding: 6px 14px;
+ font-size: 16px;
+ cursor: pointer;
+ font-family: monospace;
+ display: none;
+ transition: background 0.15s;
+ `;
+ this._closeBtn.addEventListener('mouseenter', () => { this._closeBtn.style.background = 'rgba(200,50,50,0.7)'; });
+ this._closeBtn.addEventListener('mouseleave', () => { this._closeBtn.style.background = 'rgba(0,0,0,0.6)'; });
+ this._closeBtn.addEventListener('click', () => this._hideContainer());
+ document.body.appendChild(this._closeBtn);
+
+ // Escape key handler
+ this._escHandler = (e) => {
+ if (e.key === 'Escape' && this._visible) {
+ this._hideContainer();
+ }
+ };
+ document.addEventListener('keydown', this._escHandler);
+ }
+
+ _showContainer(mode) {
+ if (!this._container) return;
+ this._mode = mode || this._mode;
+ this._visible = true;
+
+ // Show close button
+ this._closeBtn.style.display = 'block';
+
+ if (this._mode === 'top') {
+ this._applyTopPanel();
+ } else {
+ this._applyFullscreen();
+ }
+
+ this._startLoop();
+ }
+
+ _applyTopPanel() {
+ // 3D container: top 50vh
+ this._container.style.cssText = `
+ position: fixed;
+ top: 0; left: 0;
+ width: 100vw; height: 50vh;
+ z-index: 400;
+ pointer-events: auto;
+ display: block;
+ `;
+
+ // Terminal: push down
+ const term = document.getElementById('terminal-container');
+ if (term) {
+ if (!this._savedTerminalStyles) {
+ this._savedTerminalStyles = { height: term.style.height, marginTop: term.style.marginTop };
+ }
+ term.style.marginTop = '50vh';
+ term.style.height = '50vh';
+ }
+
+ // Input area: stays at bottom
+ const inputArea = document.getElementById('input-area');
+ if (inputArea) {
+ if (!this._savedInputStyles) {
+ this._savedInputStyles = { position: inputArea.style.position, bottom: inputArea.style.bottom };
+ }
+ }
+
+ // Resize renderer
+ this._resizeRenderer(window.innerWidth, window.innerHeight / 2);
+ }
+
+ _applyFullscreen() {
+ // 3D container: full viewport
+ this._container.style.cssText = `
+ position: fixed;
+ top: 0; left: 0;
+ width: 100vw; height: 100vh;
+ z-index: 400;
+ pointer-events: auto;
+ display: block;
+ `;
+
+ // Terminal: float as overlay
+ const term = document.getElementById('terminal-container');
+ if (term) {
+ if (!this._savedTerminalStyles) {
+ this._savedTerminalStyles = { height: term.style.height, marginTop: term.style.marginTop };
+ }
+ term.style.cssText = `
+ position: fixed;
+ bottom: 60px;
+ left: 20px;
+ right: 20px;
+ max-height: 50vh;
+ z-index: 401;
+ background: rgba(0,0,0,0.7);
+ backdrop-filter: blur(8px);
+ -webkit-backdrop-filter: blur(8px);
+ border-radius: 8px;
+ padding: 10px;
+ overflow-y: auto;
+ border: 1px solid #333;
+ `;
+ }
+
+ // Input area: fixed at bottom
+ const inputArea = document.getElementById('input-area');
+ if (inputArea) {
+ if (!this._savedInputStyles) {
+ this._savedInputStyles = { position: inputArea.style.position, bottom: inputArea.style.bottom };
+ }
+ inputArea.style.cssText = `
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 402;
+ background: rgba(0,0,0,0.85);
+ padding: 8px 12px;
+ border-top: 1px solid #333;
+ `;
+ }
+
+ // Header: hide
+ const header = document.querySelector('header');
+ if (header) {
+ if (!this._savedHeaderStyles) {
+ this._savedHeaderStyles = { display: header.style.display };
+ }
+ header.style.display = 'none';
+ }
+
+ this._resizeRenderer(window.innerWidth, window.innerHeight);
+ }
+
+ _restoreTerminal() {
+ const term = document.getElementById('terminal-container');
+ if (term && this._savedTerminalStyles) {
+ term.style.cssText = '';
+ term.style.height = this._savedTerminalStyles.height || '';
+ term.style.marginTop = this._savedTerminalStyles.marginTop || '';
+ term.style.position = '';
+ term.style.bottom = '';
+ term.style.left = '';
+ term.style.right = '';
+ term.style.maxHeight = '';
+ term.style.zIndex = '';
+ term.style.background = '';
+ term.style.backdropFilter = '';
+ term.style.borderRadius = '';
+ term.style.padding = '';
+ term.style.overflowY = '';
+ term.style.border = '';
+ }
+
+ const inputArea = document.getElementById('input-area');
+ if (inputArea && this._savedInputStyles) {
+ inputArea.style.cssText = '';
+ inputArea.style.position = this._savedInputStyles.position || '';
+ inputArea.style.bottom = this._savedInputStyles.bottom || '';
+ }
+
+ const header = document.querySelector('header');
+ if (header && this._savedHeaderStyles) {
+ header.style.display = this._savedHeaderStyles.display || '';
+ }
+ }
+
+ _hideContainer() {
+ if (!this._container) return;
+ this._visible = false;
+ this._container.style.display = 'none';
+ this._container.style.pointerEvents = 'none';
+ if (this._closeBtn) this._closeBtn.style.display = 'none';
+
+ // Pause animation (don't cancel — scene stays intact)
+ if (this._animId) {
+ cancelAnimationFrame(this._animId);
+ this._animId = null;
+ }
+
+ this._restoreTerminal();
+ }
+
+ _resizeRenderer(w, h) {
+ if (this._renderer && this._camera) {
+ this._camera.aspect = w / h;
+ this._camera.updateProjectionMatrix();
+ this._renderer.setSize(w, h);
+ }
+ }
+
+ // ── Scene management ──
+
+ _createScene() {
+ const T = this._THREE;
+ this._scene = new T.Scene();
+ this._scene.background = new T.Color(this._backgroundColor);
+
+ this._camera = new T.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 10000);
+ this._camera.position.set(0, 5, 15);
+
+ this._createContainer();
+ this._renderer = new T.WebGLRenderer({ antialias: true, alpha: false });
+ this._renderer.setSize(window.innerWidth, window.innerHeight);
+ this._renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ this._renderer.shadowMap.enabled = true;
+ this._container.appendChild(this._renderer.domElement);
+
+ // WebGL context loss handling
+ this._renderer.domElement.addEventListener('webglcontextlost', this._webglLostHandler = (e) => {
+ e.preventDefault();
+ this._stopLoop();
+ this.terminal?.print('⚠ WebGL context lost', 'warning');
+ });
+ this._renderer.domElement.addEventListener('webglcontextrestored', this._webglRestoredHandler = () => {
+ this._startLoop();
+ this.terminal?.print('✅ WebGL context restored', 'success');
+ });
+
+ this._controls = new T.OrbitControls(this._camera, this._renderer.domElement);
+ this._controls.enableDamping = true;
+ this._controls.dampingFactor = 0.08;
+ this._controls.minDistance = 2;
+ this._controls.maxDistance = 500;
+
+ // Lights
+ const ambient = new T.AmbientLight(0x404060);
+ this._scene.add(ambient);
+ const dirLight = new T.DirectionalLight(0xffffff, 1);
+ dirLight.position.set(10, 20, 10);
+ dirLight.castShadow = true;
+ this._scene.add(dirLight);
+ const fillLight = new T.DirectionalLight(0x8888ff, 0.4);
+ fillLight.position.set(-10, 0, -10);
+ this._scene.add(fillLight);
+
+ // Grid
+ const grid = new T.GridHelper(30, 20, 0x00ff88, 0x333355);
+ this._scene.add(grid);
+
+ // Resize handler — remove old first to prevent leak
+ if (window._threeResizeHandler) {
+ window.removeEventListener('resize', window._threeResizeHandler);
+ }
+ window._threeResizeHandler = () => {
+ if (this._camera && this._renderer && this._visible) {
+ if (this._mode === 'top') {
+ this._resizeRenderer(window.innerWidth, window.innerHeight / 2);
+ } else {
+ this._resizeRenderer(window.innerWidth, window.innerHeight);
+ }
+ }
+ };
+ window.addEventListener('resize', window._threeResizeHandler);
+
+ this._objects = [];
+ this._idCounter = 0;
+ }
+
+ _startLoop() {
+ if (this._animId) cancelAnimationFrame(this._animId);
+ const loop = () => {
+ this._animId = requestAnimationFrame(loop);
+ if (this._controls) this._controls.update();
+ if (this._renderer && this._scene && this._camera) {
+ this._renderer.render(this._scene, this._camera);
+ }
+ };
+ loop();
+ }
+
+ _stopLoop() {
+ if (this._animId) {
+ cancelAnimationFrame(this._animId);
+ this._animId = null;
+ }
+ }
+
+ // ── Lifecycle ──
+
+ destroy() {
+ this._stopLoop();
+ this._hideContainer();
+ // Remove event listeners
+ if (window._threeResizeHandler) {
+ window.removeEventListener('resize', window._threeResizeHandler);
+ window._threeResizeHandler = null;
+ }
+ if (this._escHandler) {
+ document.removeEventListener('keydown', this._escHandler);
+ this._escHandler = null;
+ }
+ if (this._renderer) {
+ if (this._webglLostHandler) {
+ this._renderer.domElement.removeEventListener('webglcontextlost', this._webglLostHandler);
+ }
+ if (this._webglRestoredHandler) {
+ this._renderer.domElement.removeEventListener('webglcontextrestored', this._webglRestoredHandler);
+ }
+ this._renderer.dispose();
+ this._renderer = null;
+ }
+ // Dispose all GPU resources
+ this._disposeAllObjects();
+ // Remove DOM elements
+ if (this._container) {
+ this._container.remove();
+ this._container = null;
+ }
+ if (this._closeBtn) {
+ this._closeBtn.remove();
+ this._closeBtn = null;
+ }
+ this._scene = null;
+ this._camera = null;
+ this._controls = null;
+ this._loaded = false;
+ this._visible = false;
+ }
+
+ _disposeAllObjects() {
+ for (const obj of this._objects) {
+ if (this._scene) this._scene.remove(obj.mesh);
+ if (obj.mesh.geometry) obj.mesh.geometry.dispose();
+ if (obj.mesh.material) {
+ const mats = Array.isArray(obj.mesh.material) ? obj.mesh.material : [obj.mesh.material];
+ mats.forEach(m => {
+ if (m.map) m.map.dispose();
+ m.dispose();
+ });
+ }
+ }
+ this._objects = [];
+ this._idCounter = 0;
+ }
+
+ _genId() {
+ return 'obj_' + (++this._idCounter);
+ }
+
+ // ── Object creation ──
+
+ _addBox(args) {
+ const T = this._THREE;
+ const size = parseFloat(args[0]) || 1;
+ const color = args[1] || '#818cf8';
+ const geo = new T.BoxGeometry(size, size, size);
+ const mat = new T.MeshStandardMaterial({ color, metalness: 0.3, roughness: 0.6 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.castShadow = true;
+ const id = this._genId();
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id, label: 'Box ' + id });
+ this.terminal.print(`Added box (${id}) at origin`, 'success');
+ return id;
+ }
+
+ _addSphere(args) {
+ const T = this._THREE;
+ const radius = parseFloat(args[0]) || 0.8;
+ const color = args[1] || '#34d399';
+ const geo = new T.SphereGeometry(radius, 32, 32);
+ const mat = new T.MeshStandardMaterial({ color, metalness: 0.2, roughness: 0.4 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.castShadow = true;
+ const id = this._genId();
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id, label: 'Sphere ' + id });
+ this.terminal.print(`Added sphere (${id}) at origin`, 'success');
+ return id;
+ }
+
+ _addTorus(args) {
+ const T = this._THREE;
+ const radius = parseFloat(args[0]) || 1;
+ const tube = parseFloat(args[1]) || 0.3;
+ const color = args[2] || '#e879f9';
+ const geo = new T.TorusGeometry(radius, tube, 16, 32);
+ const mat = new T.MeshStandardMaterial({ color, metalness: 0.4, roughness: 0.3 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.castShadow = true;
+ const id = this._genId();
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id, label: 'Torus ' + id });
+ this.terminal.print(`Added torus (${id}) at origin`, 'success');
+ return id;
+ }
+
+ _addText(args) {
+ if (!args[0]) return this.terminal.print('Usage: three add text <text> [color]', 'warning');
+ const T = this._THREE;
+ const text = args[0];
+ const color = args[1] || '#22d3ee';
+ const canvas = document.createElement('canvas');
+ canvas.width = 256;
+ canvas.height = 64;
+ const ctx = canvas.getContext('2d');
+ ctx.clearRect(0, 0, 256, 64);
+ ctx.font = 'bold 36px monospace';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = color;
+ ctx.fillText(text, 128, 32);
+ const texture = new T.CanvasTexture(canvas);
+ const mat = new T.SpriteMaterial({ map: texture, transparent: true });
+ const sprite = new T.Sprite(mat);
+ sprite.scale.set(4, 1, 1);
+ const id = this._genId();
+ this._scene.add(sprite);
+ this._objects.push({ mesh: sprite, id, label: 'Text: ' + text });
+ this.terminal.print(`Added text "${text}" (${id})`, 'success');
+ return id;
+ }
+
+ _addParticles(args) {
+ const T = this._THREE;
+ const count = parseInt(args[0]) || 500;
+ const color = args[1] || '#00ff88';
+ const geo = new T.BufferGeometry();
+ const positions = new Float32Array(count * 3);
+ for (let i = 0; i < count * 3; i++) {
+ positions[i] = (Math.random() - 0.5) * 40;
+ }
+ geo.setAttribute('position', new T.BufferAttribute(positions, 3));
+ const mat = new T.PointsMaterial({
+ color, size: 0.15, transparent: true, opacity: 0.8, blending: T.AdditiveBlending
+ });
+ const points = new T.Points(geo, mat);
+ const id = this._genId();
+ this._scene.add(points);
+ this._objects.push({ mesh: points, id, label: 'Particles (' + count + ')' });
+ this.terminal.print(`Added ${count} particles (${id})`, 'success');
+ return id;
+ }
+
+ _addLine(args) {
+ const T = this._THREE;
+ if (args.length < 6) return this.terminal.print('Usage: three add line <x1> <y1> <z1> <x2> <y2> <z2> [color]', 'warning');
+ const x1 = parseFloat(args[0]), y1 = parseFloat(args[1]), z1 = parseFloat(args[2]);
+ const x2 = parseFloat(args[3]), y2 = parseFloat(args[4]), z2 = parseFloat(args[5]);
+ const color = args[6] || '#00ff88';
+ const points = [
+ new T.Vector3(x1, y1, z1),
+ new T.Vector3(x2, y2, z2)
+ ];
+ const geo = new T.BufferGeometry().setFromPoints(points);
+ const mat = new T.LineBasicMaterial({ color });
+ const line = new T.Line(geo, mat);
+ const id = this._genId();
+ this._scene.add(line);
+ this._objects.push({ mesh: line, id, label: 'Line ' + id });
+ this.terminal.print(`Added line (${id})`, 'success');
+ return id;
+ }
+
+ // ── Position helpers ──
+
+ _setPosition(id, x, y, z) {
+ const obj = this._objects.find(o => o.id === id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ obj.mesh.position.set(x, y, z);
+ this.terminal.print(`Moved ${id} to (${x}, ${y}, ${z})`, 'success');
+ }
+
+ _setRotation(id, rx, ry, rz) {
+ const obj = this._objects.find(o => o.id === id);
+ if (!obj) return this.terminal.print('Object not found: ' + id, 'error');
+ obj.mesh.rotation.set(rx, ry, rz);
+ this.terminal.print(`Rotated ${id}`, 'success');
+ }
+
+ // ── 3D Mindmap ──
+
+ _renderMindmap(args) {
+ const mm = this.terminal.moduleManager.modules['MindmapModule'];
+ if (!mm) return this.terminal.print('MindmapModule not loaded', 'error');
+ const cur = mm._ensureMindmap();
+ if (!cur) return this.terminal.print('No mindmap. Create one with mindmap new first.', 'warning');
+ const root = cur.root;
+
+ if (!this._scene) this._createScene();
+ this._showContainer(this._mode);
+ this._clearScene();
+
+ const T = this._THREE;
+ const colorMap = {
+ red: '#fb7185', green: '#34d399', yellow: '#fbbf24', blue: '#818cf8',
+ cyan: '#22d3ee', magenta: '#e879f9', white: '#e0e0e0'
+ };
+
+ const nodes = [];
+ const edges = [];
+
+ const layout = (node, parentPos, depth, angle, spread) => {
+ const pos = { x: 0, y: 0, z: 0 };
+ if (!parentPos) {
+ pos.x = 0; pos.y = 4; pos.z = 0;
+ } else {
+ const radius = 2 + depth * 2.5;
+ pos.x = parentPos.x + Math.cos(angle) * radius;
+ pos.z = parentPos.z + Math.sin(angle) * radius;
+ pos.y = parentPos.y - 1.5;
+ }
+ nodes.push({ ...node, pos, depth, color: colorMap[node.color] || '#818cf8' });
+ if (parentPos) edges.push({ from: parentPos, to: pos });
+ if (node.children) {
+ const count = node.children.length;
+ const startAngle = angle - spread / 2;
+ node.children.forEach((child, i) => {
+ const childAngle = startAngle + (i / (count || 1)) * spread;
+ layout(child, pos, depth + 1, childAngle, Math.min(spread * 0.7, Math.PI));
+ });
+ }
+ };
+ layout(root, null, 0, 0, Math.PI * 1.5);
+
+ const nodeMeshes = [];
+ for (const n of nodes) {
+ const radius = n.depth === 0 ? 0.6 : 0.3 + (1 / (n.depth + 1)) * 0.4;
+ const geo = new T.SphereGeometry(radius, 16, 16);
+ const mat = new T.MeshStandardMaterial({
+ color: n.color, emissive: n.color, emissiveIntensity: 0.15,
+ metalness: 0.3, roughness: 0.5
+ });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.position.set(n.pos.x, n.pos.y, n.pos.z);
+ mesh.castShadow = true;
+ mesh.userData = { id: n.id, text: n.text, depth: n.depth };
+ this._scene.add(mesh);
+ nodeMeshes.push(mesh);
+ this._objects.push({ mesh, id: this._genId(), label: 'Mindmap: ' + n.text });
+
+ // Text label sprite
+ const canvas = document.createElement('canvas');
+ canvas.width = 512; canvas.height = 96;
+ const ctx = canvas.getContext('2d');
+ ctx.font = 'bold 36px monospace';
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
+ ctx.fillStyle = n.color;
+ const label = n.text.length > 16 ? n.text.substring(0, 14) + '..' : n.text;
+ ctx.fillText(label, 256, 48);
+ const tex = new T.CanvasTexture(canvas);
+ const spriteMat = new T.SpriteMaterial({ map: tex, transparent: true, depthTest: false });
+ const sprite = new T.Sprite(spriteMat);
+ sprite.position.set(n.pos.x, n.pos.y + radius + 0.5, n.pos.z);
+ sprite.scale.set(2.5, 0.5, 1);
+ this._scene.add(sprite);
+ this._objects.push({ mesh: sprite, id: this._genId(), label: 'Label: ' + n.text });
+ }
+
+ for (const e of edges) {
+ const pts = [
+ new T.Vector3(e.from.x, e.from.y, e.from.z),
+ new T.Vector3((e.from.x + e.to.x) / 2, (e.from.y + e.to.y) / 2 - 0.5, (e.from.z + e.to.z) / 2),
+ new T.Vector3(e.to.x, e.to.y, e.to.z)
+ ];
+ const curve = new T.QuadraticBezierCurve3(pts[0], pts[1], pts[2]);
+ const curvePoints = curve.getPoints(20);
+ const geo = new T.BufferGeometry().setFromPoints(curvePoints);
+ const mat = new T.LineBasicMaterial({ color: 0x444466, transparent: true, opacity: 0.5 });
+ const line = new T.Line(geo, mat);
+ this._scene.add(line);
+ this._objects.push({ mesh: line, id: this._genId(), label: 'Edge' });
+ }
+
+ if (nodeMeshes.length > 0) {
+ const box = new T.Box3().setFromObjects(nodeMeshes);
+ const center = box.getCenter(new T.Vector3());
+ const size = box.getSize(new T.Vector3());
+ const maxDim = Math.max(size.x, size.y, size.z);
+ const dist = maxDim * 1.5 + 5;
+ this._camera.position.set(center.x + dist * 0.5, center.y + dist * 0.3, center.z + dist);
+ this._controls.target.copy(center);
+ this._controls.update();
+ }
+
+ this.terminal.print(`🧊 3D mindmap rendered: ${nodes.length} nodes, ${edges.length} edges`, 'success');
+ }
+
+ // ── Presets ──
+
+ _presetSolarSystem() {
+ if (!this._scene) this._createScene();
+ this._showContainer(this._mode);
+ this._clearScene();
+ const T = this._THREE;
+
+ const sunGeo = new T.SphereGeometry(1.5, 32, 32);
+ const sunMat = new T.MeshStandardMaterial({ color: 0xffaa00, emissive: 0xff6600, emissiveIntensity: 0.5 });
+ const sun = new T.Mesh(sunGeo, sunMat);
+ this._scene.add(sun);
+ this._objects.push({ mesh: sun, id: this._genId(), label: 'Sun' });
+
+ const planets = [
+ { name: 'Mercury', radius: 0.15, orbit: 2.5, color: 0xaaaaaa, speed: 0.02 },
+ { name: 'Venus', radius: 0.25, orbit: 4, color: 0xffcc88, speed: 0.015 },
+ { name: 'Earth', radius: 0.3, orbit: 5.5, color: 0x4488ff, speed: 0.01 },
+ { name: 'Mars', radius: 0.2, orbit: 7, color: 0xff6644, speed: 0.008 },
+ { name: 'Jupiter', radius: 0.6, orbit: 9, color: 0xddbb88, speed: 0.005 },
+ { name: 'Saturn', radius: 0.5, orbit: 11, color: 0xeeddbb, speed: 0.003 },
+ ];
+ for (const p of planets) {
+ const geo = new T.SphereGeometry(p.radius, 16, 16);
+ const mat = new T.MeshStandardMaterial({ color: p.color });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.position.x = p.orbit;
+ mesh.userData = { orbit: p.orbit, speed: p.speed, angle: Math.random() * Math.PI * 2 };
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id: this._genId(), label: p.name, _animate: true, _data: mesh.userData });
+
+ const ringPoints = [];
+ for (let i = 0; i <= 64; i++) {
+ const a = (i / 64) * Math.PI * 2;
+ ringPoints.push(new T.Vector3(Math.cos(a) * p.orbit, 0, Math.sin(a) * p.orbit));
+ }
+ const ringGeo = new T.BufferGeometry().setFromPoints(ringPoints);
+ const ringMat = new T.LineBasicMaterial({ color: 0x333355, transparent: true, opacity: 0.3 });
+ const ring = new T.Line(ringGeo, ringMat);
+ this._scene.add(ring);
+ this._objects.push({ mesh: ring, id: this._genId(), label: 'Orbit: ' + p.name });
+ }
+ this.terminal.print('🌌 Solar system preset loaded', 'success');
+ }
+
+ _presetGrid(args) {
+ if (!this._scene) this._createScene();
+ this._showContainer(this._mode);
+ this._clearScene();
+ const T = this._THREE;
+ const size = parseInt(args[0]) || 5;
+ const spacing = parseFloat(args[1]) || 2;
+
+ for (let x = -size; x <= size; x++) {
+ for (let z = -size; z <= size; z++) {
+ const geo = new T.BoxGeometry(0.3, 0.3, 0.3);
+ const hue = ((x + size) / (size * 2)) * 0.6 + 0.5;
+ const color = new T.Color().setHSL(hue, 0.8, 0.5);
+ const mat = new T.MeshStandardMaterial({ color, emissive: color, emissiveIntensity: 0.1 });
+ const mesh = new T.Mesh(geo, mat);
+ mesh.position.set(x * spacing, 0, z * spacing);
+ this._scene.add(mesh);
+ this._objects.push({ mesh, id: this._genId(), label: `Grid (${x},${z})` });
+ }
+ }
+ this.terminal.print(`📐 Grid preset: ${(size*2+1)**2} cubes`, 'success');
+ }
+
+ // ── Export ──
+
+ _export(args) {
+ if (!this._scene) return this.terminal.print('No scene to export', 'warning');
+ const rawName = args[0] || 'three-scene';
+ const name = rawName.replace(/[<>&"']/g, function(c) { return {'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;',"'":'&#39;'}[c]; });
+ const T = this._THREE;
+
+ const objects = [];
+ for (const obj of this._objects) {
+ const m = obj.mesh;
+ if (m.type === 'Mesh' || m.type === 'Points' || m.type === 'Sprite' || m.type === 'Line') {
+ const hexColor = m.material && m.material.color ? m.material.color.getHex() : null;
+ objects.push({
+ type: m.type, id: obj.id, label: obj.label,
+ position: m.position.toArray(), rotation: m.rotation.toArray(), scale: m.scale.toArray(),
+ color: hexColor
+ });
+ }
+ }
+
+ const cameraPos = this._camera ? this._camera.position.toArray() : [0, 5, 15];
+ const bgColor = '#' + new T.Color(this._backgroundColor).getHexString();
+
+ const html = `<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>3D Scene: ${name}</title>
+ <script src="three.min.js"></script>
+ <script src="OrbitControls.js"></script>
+ <style>
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ body { background: ${bgColor}; overflow: hidden; font-family: monospace; }
+ #info {
+ position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
+ color: #888; font-size: 12px; background: rgba(0,0,0,0.6);
+ padding: 8px 16px; border-radius: 8px; pointer-events: none;
+ border: 1px solid #333;
+ }
+ </style>
+</head>
+<body>
+ <div id="info">${name} — ${objects.length} objects — Drag to orbit, scroll to zoom</div>
+ <script>
+ const scene = new THREE.Scene();
+ scene.background = new THREE.Color('${bgColor}');
+ const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 10000);
+ camera.position.set(${cameraPos.join(', ')});
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
+ document.body.appendChild(renderer.domElement);
+ const controls = new THREE.OrbitControls(camera, renderer.domElement);
+ controls.enableDamping = true;
+ scene.add(new THREE.AmbientLight(0x404060));
+ const dl = new THREE.DirectionalLight(0xffffff, 1);
+ dl.position.set(10, 20, 10);
+ scene.add(dl);
+ const fl = new THREE.DirectionalLight(0x8888ff, 0.4);
+ fl.position.set(-10, 0, -10);
+ scene.add(fl);
+ scene.add(new THREE.GridHelper(30, 20, 0x00ff88, 0x333355));
+ const data = ${JSON.stringify(objects)};
+ data.forEach(function(d) {
+ var mesh;
+ if (d.type === 'Mesh') {
+ var geo = new THREE.SphereGeometry(0.5, 16, 16);
+ var mat = new THREE.MeshStandardMaterial({ color: d.color != null ? d.color : 0x818cf8 });
+ mesh = new THREE.Mesh(geo, mat);
+ } else if (d.type === 'Points') {
+ var geo = new THREE.BufferGeometry();
+ var pos = new Float32Array(500 * 3);
+ for (var i = 0; i < 1500; i++) pos[i] = (Math.random() - 0.5) * 40;
+ geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
+ var mat = new THREE.PointsMaterial({ color: d.color != null ? d.color : 0x00ff88, size: 0.15, transparent: true });
+ mesh = new THREE.Points(geo, mat);
+ } else if (d.type === 'Sprite') {
+ var canvas = document.createElement('canvas');
+ canvas.width = 512; canvas.height = 96;
+ var ctx = canvas.getContext('2d');
+ ctx.font = 'bold 36px monospace';
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
+ ctx.fillStyle = d.color != null ? '#' + d.color.toString(16).padStart(6, '0') : '#22d3ee';
+ ctx.fillText(d.label || '?', 256, 48);
+ var tex = new THREE.CanvasTexture(canvas);
+ var mat = new THREE.SpriteMaterial({ map: tex, transparent: true });
+ mesh = new THREE.Sprite(mat);
+ } else { return; }
+ mesh.position.set(d.position[0], d.position[1], d.position[2]);
+ if (d.rotation) mesh.rotation.set(d.rotation[0], d.rotation[1], d.rotation[2]);
+ if (d.scale) mesh.scale.set(d.scale[0], d.scale[1], d.scale[2]);
+ scene.add(mesh);
+ });
+ function animate() {
+ requestAnimationFrame(animate);
+ controls.update();
+ renderer.render(scene, camera);
+ }
+ animate();
+ window.addEventListener('resize', function() {
+ camera.aspect = window.innerWidth / window.innerHeight;
+ camera.updateProjectionMatrix();
+ renderer.setSize(window.innerWidth, window.innerHeight);
+ });
+ </script>
+</body>
+</html>`;
+
+ try {
+ const fs = this.terminal.fs;
+ fs.mkdirRecursive('/exports');
+ fs.writeFile('/exports/' + name + '.html', html);
+ this.terminal.print(`💾 Exported to /exports/${name}.html (${(html.length / 1024).toFixed(1)}KB)`, 'success');
+ } catch (e) {
+ this.terminal.print('Failed to save export: ' + e.message, 'error');
+ }
+ }
+
+ // ── Scene helpers ──
+
+ _clearScene() {
+ if (!this._scene) return;
+ this._disposeAllObjects();
+ }
+
+ _listObjects() {
+ if (this._objects.length === 0) return this.terminal.print('No objects in scene', 'info');
+ this.terminal.print(`=== 3D Objects (${this._objects.length}) ===`, 'header');
+ for (const obj of this._objects) {
+ const pos = obj.mesh.position;
+ this.terminal.print(` ${obj.id} ${obj.label} @ (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})`);
+ }
+ }
+
+ _removeObject(id) {
+ const idx = this._objects.findIndex(o => o.id === id);
+ if (idx === -1) return this.terminal.print('Object not found: ' + id, 'error');
+ const obj = this._objects[idx];
+ this._scene.remove(obj.mesh);
+ if (obj.mesh.geometry) obj.mesh.geometry.dispose();
+ if (obj.mesh.material) {
+ const mats = Array.isArray(obj.mesh.material) ? obj.mesh.material : [obj.mesh.material];
+ mats.forEach(m => {
+ if (m.map) m.map.dispose();
+ m.dispose();
+ });
+ }
+ this._objects.splice(idx, 1);
+ this.terminal.print(`Removed ${id}`, 'success');
+ }
+
+ _setBgColor(args) {
+ if (!args[0]) return this.terminal.print('Usage: three bgcolor <hex> (e.g. #0a0a14)', 'warning');
+ const color = parseInt(args[0].replace('#', ''), 16);
+ if (isNaN(color)) return this.terminal.print('Invalid color', 'warning');
+ this._backgroundColor = color;
+ if (this._scene) this._scene.background = new this._THREE.Color(color);
+ this.terminal.print(`Background set to ${args[0]}`, 'success');
+ }
+
+ // ── Main dispatch ──
+
+ async _dispatch(args) {
+ if (!args[0] || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
+ this._printHelp();
+ return;
+ }
+
+ const loaded = await this._ensureLoaded();
+ if (!loaded && args[0] !== 'help') return;
+
+ const sub = args[0];
+ const rest = args.slice(1);
+
+ switch (sub) {
+ case 'scene':
+ if (!this._scene) this._createScene();
+ this._mode = 'top';
+ this._showContainer('top');
+ this.terminal.print('🎨 3D scene — top panel mode. Use three fullscreen for full view, Esc to close.', 'success');
+ break;
+ case 'fullscreen':
+ if (!this._scene) this._createScene();
+ this._mode = 'full';
+ this._showContainer('full');
+ this.terminal.print('🎨 3D scene — fullscreen mode. Terminal floats on top. Esc to close.', 'success');
+ break;
+ case 'mode': {
+ const mode = rest[0];
+ if (!mode || (mode !== 'top' && mode !== 'full')) {
+ return this.terminal.print('Usage: three mode top|full (current: ' + this._mode + ')', 'warning');
+ }
+ if (!this._scene) return this.terminal.print('No scene. Create one with three scene first.', 'warning');
+ this._mode = mode;
+ this._showContainer(mode);
+ this.terminal.print(`Switched to ${mode} panel mode`, 'success');
+ break;
+ }
+ case 'add': {
+ const type = rest[0];
+ const params = rest.slice(1);
+ if (!type) return this.terminal.print('Usage: three add box|sphere|torus|text|particles|line [params...]', 'warning');
+ if (!this._scene) this._createScene();
+ if (!this._visible) this._showContainer(this._mode);
+ switch (type) {
+ case 'box': this._addBox(params); break;
+ case 'sphere': this._addSphere(params); break;
+ case 'torus': this._addTorus(params); break;
+ case 'text': this._addText(params); break;
+ case 'particles': this._addParticles(params); break;
+ case 'line': this._addLine(params); break;
+ default: this.terminal.print('Unknown type: ' + type + '. Try: box, sphere, torus, text, particles, line', 'warning');
+ }
+ break;
+ }
+ case 'camera': {
+ if (rest.length < 3) return this.terminal.print('Usage: three camera <x> <y> <z>', 'warning');
+ if (!this._camera) return this.terminal.print('No scene. Create one with three scene first.', 'warning');
+ this._camera.position.set(parseFloat(rest[0]), parseFloat(rest[1]), parseFloat(rest[2]));
+ this._controls.update();
+ this.terminal.print(`Camera moved to (${rest[0]}, ${rest[1]}, ${rest[2]})`, 'success');
+ break;
+ }
+ case 'orbit': {
+ if (!this._controls) return this.terminal.print('No scene.', 'warning');
+ this._orbitEnabled = !this._orbitEnabled;
+ this._controls.enabled = this._orbitEnabled;
+ this.terminal.print(`Orbit controls: ${this._orbitEnabled ? 'ON' : 'OFF'}`, 'info');
+ break;
+ }
+ case 'clear':
+ this._clearScene();
+ this.terminal.print('Scene cleared', 'success');
+ break;
+ case 'list':
+ this._listObjects();
+ break;
+ case 'remove':
+ if (!rest[0]) return this.terminal.print('Usage: three remove <id>', 'warning');
+ this._removeObject(rest[0]);
+ break;
+ case 'pos':
+ if (rest.length < 4) return this.terminal.print('Usage: three pos <id> <x> <y> <z>', 'warning');
+ this._setPosition(rest[0], parseFloat(rest[1]), parseFloat(rest[2]), parseFloat(rest[3]));
+ break;
+ case 'mindmap':
+ this._renderMindmap(rest);
+ break;
+ case 'export':
+ this._export(rest);
+ break;
+ case 'preset': {
+ const preset = rest[0] || 'solar';
+ if (!this._scene) this._createScene();
+ if (!this._visible) this._showContainer(this._mode);
+ if (preset === 'solar') this._presetSolarSystem();
+ else if (preset === 'grid') this._presetGrid(rest.slice(1));
+ else this.terminal.print('Presets: solar, grid', 'warning');
+ break;
+ }
+ case 'bgcolor':
+ this._setBgColor(rest);
+ break;
+ case 'hide':
+ this._hideContainer();
+ this.terminal.print('3D view closed. Use three scene or three fullscreen to reopen.', 'info');
+ break;
+ default:
+ this.terminal.print('Unknown subcommand: ' + sub + '. Try: three help', 'warning');
+ }
+ }
+
+ _printHelp() {
+ this.terminal.print('=== Three.js 3D Module (v4.5.82) ===', 'header');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Scene ──', 'info');
+ this.terminal.print(' three scene Open 3D in top panel (default)');
+ this.terminal.print(' three fullscreen Open 3D fullscreen with terminal overlay');
+ this.terminal.print(' three mode top|full Switch mode without closing');
+ this.terminal.print(' three hide Close 3D view (or press Esc)');
+ this.terminal.print(' three clear Remove all objects');
+ this.terminal.print(' three bgcolor <hex> Set background color (e.g. #0a0a14)');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Objects ──', 'info');
+ this.terminal.print(' three add box [size] [color] Add a box');
+ this.terminal.print(' three add sphere [r] [color] Add a sphere');
+ this.terminal.print(' three add torus [r] [tube] [c] Add a torus');
+ this.terminal.print(' three add text <text> [color] Add 2D text sprite');
+ this.terminal.print(' three add particles [n] [c] Add particle system');
+ this.terminal.print(' three add line x1 y1 z1 x2 y2 z2 [color] Add a line');
+ this.terminal.print(' three list List all objects');
+ this.terminal.print(' three remove <id> Remove an object');
+ this.terminal.print(' three pos <id> <x> <y> <z> Move an object');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Camera & Controls ──', 'info');
+ this.terminal.print(' three camera <x> <y> <z> Set camera position');
+ this.terminal.print(' three orbit Toggle orbit controls');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Mindmap ──', 'info');
+ this.terminal.print(' three mindmap [name] Render current mindmap in 3D');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Presets ──', 'info');
+ this.terminal.print(' three preset solar Solar system');
+ this.terminal.print(' three preset grid [n] [spc] Color grid of cubes');
+ this.terminal.print('', '');
+ this.terminal.print(' ── Export ──', 'info');
+ this.terminal.print(' three export [name] Export scene as standalone HTML');
+ this.terminal.print('', '');
+ this.terminal.print(' ── AI Tools ──', 'info');
+ this.terminal.print(' The AI can create 3D scenes via create_3d_scene tool.');
+ this.terminal.print(' The AI can render mindmaps in 3D via visualize_mindmap_3d tool.');
+ }
+
+ // ── AI Tool Interface ──
+
+ async createSceneFromDescription(description) {
+ const loaded = await this._ensureLoaded();
+ if (!loaded) return 'Failed to load Three.js';
+ if (!this._scene) this._createScene();
+ this._showContainer(this._mode);
+ this._clearScene();
+
+ const T = this._THREE;
+ const desc = description.toLowerCase();
+ const results = [];
+
+ const boxMatch = desc.match(/(\d+)?\s*box(?:es)?/);
+ if (boxMatch) {
+ const count = parseInt(boxMatch[1]) || 1;
+ for (let i = 0; i < count; i++) {
+ const id = this._addBox(['0.8', '#818cf8']);
+ const angle = (i / count) * Math.PI * 2;
+ this._setPosition(id, Math.cos(angle) * 3, 0, Math.sin(angle) * 3);
+ results.push('box ' + id);
+ }
+ }
+
+ const sphereMatch = desc.match(/(\d+)?\s*sphere(?:s)?/);
+ if (sphereMatch) {
+ const count = parseInt(sphereMatch[1]) || 1;
+ for (let i = 0; i < count; i++) {
+ const id = this._addSphere(['0.6', '#34d399']);
+ const angle = (i / count) * Math.PI * 2 + 0.5;
+ this._setPosition(id, Math.cos(angle) * 3, 1, Math.sin(angle) * 3);
+ results.push('sphere ' + id);
+ }
+ }
+
+ if (desc.includes('particles') || desc.includes('stars') || desc.includes('starfield')) {
+ this._addParticles(['800', '#00ff88']);
+ results.push('particles');
+ }
+
+ const textMatch = desc.match(/text["']?[:]?\s*["']?([^"']+)["']?/);
+ if (textMatch) {
+ this._addText([textMatch[1].trim(), '#22d3ee']);
+ results.push('text');
+ }
+
+ if (desc.includes('torus') || desc.includes('donut') || desc.includes('ring')) {
+ this._addTorus(['1.2', '0.3', '#e879f9']);
+ results.push('torus');
+ }
+
+ if (desc.includes('solar') || desc.includes('planet')) {
+ this._presetSolarSystem();
+ return 'Created solar system preset';
+ }
+
+ if (desc.includes('grid')) {
+ this._presetGrid(['4', '2']);
+ return 'Created grid preset';
+ }
+
+ if (results.length === 0) {
+ this._addBox(['1', '#818cf8']);
+ this._addSphere(['0.6', '#34d399']);
+ this._setPosition('obj_1', -2, 0, 0);
+ this._setPosition('obj_2', 2, 0, 0);
+ results.push('default scene');
+ }
+
+ return 'Created 3D scene with: ' + results.join(', ');
+ }
+
+ async visualizeMindmap3D(name) {
+ const loaded = await this._ensureLoaded();
+ if (!loaded) return 'Failed to load Three.js';
+ const mm = this.terminal.moduleManager.modules['MindmapModule'];
+ if (!mm) return 'MindmapModule not loaded';
+ const cur = mm._ensureMindmap();
+ if (!cur) return 'No mindmap active. Create one first.';
+ this._renderMindmap(name ? [name] : []);
+ const count = this._objects.filter(o => o.label.startsWith('Mindmap:')).length;
+ return `Rendered mindmap "${cur.root.text}" in 3D: ${count} nodes`;
+ }
+}
+
+// Register
+if (typeof window.webx11 !== 'undefined' && window.webx11) {
+ webx11.moduleManager.registerModule(new ThreeModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/three.js');
+}
diff --git a/packages.json b/packages.json
index 368b67a..f232334 100644
--- a/packages.json
+++ b/packages.json
@@ -279,6 +279,16 @@
"analyze"
],
"category": "utility"
+ },
+ "three": {
+ "name": "3D Scene Builder",
+ "version": "1.0.0",
+ "desc": "Three.js 3D scene builder — create, edit, visualize mindmaps in 3D, export standalone HTML",
+ "url": "modules/ext/three.js",
+ "commands": [
+ "three"
+ ],
+ "category": "ui"
}
}
} \ No newline at end of file
diff --git a/review-newos4.html b/review-newos4.html
index b82981b..975a0e8 100644
--- a/review-newos4.html
+++ b/review-newos4.html
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="version" content="4.5.75-newos4">
+ <meta name="version" content="4.5.82-newos4">
<title>ChatStudio newOS4 - Review</title>
<style>
:root {
@@ -165,39 +165,39 @@
<div class="version-header">
<span class="version-icon">🧩</span>
- <span class="version-title">newOS4 v4.5.75 &mdash; Terminal OS</span>
+ <span class="version-title">newOS4 v4.5.82 &mdash; Terminal OS</span>
<span class="version-badge" style="background:#a855f7;">Most Advanced</span>
</div>
<div class="meta-info">
- <span>v4.5.75</span>
- <span>1,341 lines (main.html skeleton, 64KB)</span>
- <span>3 bundle scripts + 8 standalone ext modules</span>
+ <span>v4.5.82</span>
+ <span>1,375 lines (main.html skeleton, 68KB)</span>
+ <span>3 bundle scripts + 22 standalone ext modules</span>
<span>12 AI-managed skills (17 in index)</span>
- <span>~936KB total bundle size</span>
+ <span>~1,020KB total bundle size</span>
</div>
<div class="section">
<h2>Architecture</h2>
<p style="color: var(--text-muted); margin-bottom: 1rem;">
- A full in-browser terminal OS with bundle-based loading. The main.html (64KB) is a lean skeleton — all functionality is bundled into 2 optimized scripts (core-bundle.js &amp; critical-ext-bundle.js) plus module-loader.js, with additional standalone ext modules loaded on demand.
+ A full in-browser terminal OS with bundle-based loading. The main.html (68KB) is a lean skeleton — all functionality is bundled into 2 optimized scripts (core-bundle.js &amp; critical-ext-bundle.js) plus module-loader.js, with 22 additional standalone ext modules loaded on demand.
</p>
<div class="arch-grid">
<div class="arch-item">
<h4>🧠 Core Bundle</h4>
- <p>core-bundle.js (828KB, 16,147 lines) — all 33 core module classes: VirtualFileSystem, ModuleManager, OllamaManager, SecretVault, WebX11Terminal, WebX11SecurityManager, plus 27 module classes (system, fs, ollama, aiassist, searxng, config, smartctx, skills, chatstudio overlay, agent-registry, memory-graph, neural-vfs, leases, orchestrator, predictor, sandbox, world-model, and more).</p>
+ <p>core-bundle.js (904KB, 17,202 lines) — 17 core classes: VirtualFileSystem, ModuleManager, OllamaManager, SecretVault, WebX11Terminal, WebX11SecurityManager, plus 11 module classes (system, fs, ollama, aiassist, searxng, config, smartctx, servermon, simplegui, duckduckgo, fetcher) and supporting classes (ollama-manager, virtual-fs, terminal, secret-vault, module-manager).</p>
</div>
<div class="arch-item">
<h4>🔧 Critical Ext Bundle</h4>
- <p>critical-ext-bundle.js (104KB, 2,368 lines) — 12 bundled ext modules: alias, busybox, calc, console, cron, cryptogzip, diff, editor, filemgr, finder, shell, tutorial.</p>
+ <p>critical-ext-bundle.js (112KB, 2,541 lines) — 12 bundled ext modules: alias, busybox, calc, console, cron, cryptogzip, diff, editor, filemgr, finder, shell, tutorial.</p>
</div>
<div class="arch-item">
<h4>🧩 Standalone Ext Modules</h4>
- <p>8 standalone files in ext/: floating.js, mindmap.js, skill-pack-core.js, theme.js, voice.js, webdav.js, wpm.js, debug-panel.js — loaded on demand by module-loader. Configurable via localStorage startup preferences.</p>
+ <p>22 standalone files in ext/ loaded on demand: alias, busybox, calc, console, cron, cryptogzip, debug-panel, diff, editor, filemgr, finder, floating, media, mindmap, shell, skill-pack-core, theme, three, tutorial, voice, webdav, wpm. Configurable via localStorage startup preferences.</p>
</div>
<div class="arch-item">
<h4>📚 Skills System</h4>
- <p>12 skill markdown files in VFS /skills/, 17 entries in index.json: navigating-the-terminal, memory-and-profile, web-search-and-fetch, module-management, system-maintenance, backup-and-restore, security-and-safety, debug-panel-and-logs, agent-orchestration, anti-loop-protocol, neural-vfs-management, mindmap-workflow, and more. AI-managed read/suggest/apply loop.</p>
+ <p>12 skill markdown files in VFS /skills/, 17 entries in index.json. AI-managed read/suggest/apply loop with 30-min debounce on suggestions.</p>
</div>
<div class="arch-item">
<h4>🎨 Theme Presets</h4>
@@ -259,7 +259,7 @@
</div>
<div class="feature-card">
<h3>🧩 Bundle-Based Loading</h3>
- <p>3 script loads with splash progress bar (core-bundle → critical-ext-bundle → init). Standalone ext modules loaded on demand. Auto-timestamp cache busting.</p>
+ <p>3 script loads with splash progress bar (core-bundle → critical-ext-bundle → init). 22 standalone ext modules loaded on demand. Auto-timestamp cache busting.</p>
</div>
</div>
</div>
@@ -289,13 +289,13 @@
<h3>Cons</h3>
<ul>
<li>Ollama dependency — requires Ollama backend for AI features</li>
- <li>Large bundle size (~936KB across all scripts)</li>
+ <li>Large bundle size (~1,020KB across all scripts)</li>
<li>No streaming AI responses (request/response cycle only)</li>
<li>No PDF support</li>
<li>Single-language (English only)</li>
<li>No Plan Mode (Stable variant has this)</li>
<li>Limited mobile optimization (desktop-first design)</li>
- <li>Complex architecture — bundle-based loading requires understanding of core-bundle + ext-bundle + standalone modules</li>
+ <li>Complex architecture — bundle-based loading requires understanding of core-bundle + ext-bundle + 22 standalone modules</li>
</ul>
</div>
</div>
@@ -304,7 +304,7 @@
<div class="section">
<h2>Verdict</h2>
<p style="color: var(--text-muted); margin-bottom: 0.5rem;">
- <strong>newOS4 v4.5.75</strong> is the most advanced ChatStudio variant — an in-browser terminal OS with agentic AI, a virtual file system, and self-improving skills. It's designed for power users, developers, and AI enthusiasts who want maximum control and capability. The bundle-based architecture (core-bundle + critical-ext-bundle + standalone modules) keeps script loading efficient while maintaining flexibility.
+ <strong>newOS4 v4.5.82</strong> is the most advanced ChatStudio variant — an in-browser terminal OS with agentic AI, a virtual file system, and self-improving skills. It's designed for power users, developers, and AI enthusiasts who want maximum control and capability. The bundle-based architecture (core-bundle + critical-ext-bundle + 22 standalone modules) keeps script loading efficient while maintaining flexibility.
</p>
<div class="rating">🧩🧩🧩🧩 — <strong>4/5</strong> — Most advanced, consistently improved</div>
</div>
@@ -412,4 +412,4 @@
<a href="/spaces/newos4/main.html" class="launch-btn">Launch newOS4</a>
</div>
</body>
-</html> \ No newline at end of file
+</html>
diff --git a/v4.5.80 b/v4.5.80
new file mode 100644
index 0000000..9a60833
--- /dev/null
+++ b/v4.5.80
@@ -0,0 +1,43 @@
+# v4.5.80 Release Notes
+
+## Session 1 — AI Debug Visibility
+
+**Problem:** Empty DeepSeek responses, no visibility into LLM I/O, debug console empty, tool calls unnamed in logs, `ai-compress` did not archive.
+
+### Shipped
+
+- `debug-ai-last` — shows the last AI response as an HTML card with a **Copy Raw** button, content truncation indicator, tool call list, and yellow/red highlights for empty responses or errors.
+- `debug-ai-send` — displays exactly what was sent to the AI, including all message roles.
+- `debug-ai-log` — dumps the full `_toolCallLog` array to the terminal, showing turn#, tool name, arguments, duration, and error status.
+- **DeepSeek empty-response logging** — when DeepSeek returns empty, the model, message count, and tool count are logged; stream retry success/failure is also logged.
+- **Named tool calls in logs** — logs now read `Used 2 tool(s): get_status, session_save` instead of `Used 2 tool(s)`.
+- `ai-compress` now archives to `/memory/archive/` with a **30-file rotation**.
+- **Auto-archive** every 10 user messages so recent snapshots exist even if context compression never fires.
+- **Debug panel tabs** — the `debug-ai` floating panel now has two tabs: 🔧 Tools and 📡 Raw I/O.
+- **Auto-truncate before DeepSeek retry** — on an empty DeepSeek response, context is truncated to the system prompt + the last 5 messages before a stream retry.
+- `archive-list` / `archive-read` commands for browsing archived session snapshots.
+- Documentation updated in `help.html` and `cheatsheet.html`.
+
+---
+
+## Session 2 — System Audit Fixes
+
+### Shipped
+
+| Phase | ID | Change |
+|-------|----|--------|
+| A | F1 | `nvfs-resolve` now auto-syncs dirty paths before resolving, eliminating stale index results. |
+| B | E | **Lazy-load ext modules** — only `debug-panel` loads at boot; other ext modules load on first use via a `dispatchCommand` intercept. The package index is cached in `localStorage`. |
+| C | D | `get_status` now returns an `agents` object containing `activeAgents`, `busyAgents`, `failedAgents`, and `registeredAgents`. |
+| D | C | `workflow-list` and `workflow-run` commands — 5 workflow templates: `daily-health-report`, `predict-and-monitor`, `compress-archives`, `media-summarize`, and `sandbox-test`. |
+| E | F2 | **Skipped** — `ModuleManagerModule` is a core builtin; no duplicate `module-modmgr` ext file exists to remove. |
+
+### Deferred
+
+- Skill file `/skills/automation-workflows.md` — the workflow commands exist, but the markdown skill file was not created initially. Workflows are currently hardcoded in JS. (Created separately if requested.)
+
+---
+
+## Verification
+
+All shipped features were verified in the browser against the live module source. The only deferred item is the automation-workflows skill file.