2026-07-16 13:06:33 +00:00
|
|
|
|
import { Editor } from '../../editor/Editor.js';
|
2026-07-25 20:50:14 +00:00
|
|
|
|
import { EditorPanelDefinition } from '../../editor/editor-panel.js';
|
2026-07-10 18:18:15 +00:00
|
|
|
|
import { ContextMenuDirectory, ContextMenuReadOnlyEntry } from '../context-menu/context-menu.js';
|
|
|
|
|
|
|
|
|
|
|
|
declare const markdownit: ( options?: Record<string, unknown> ) => { render: ( md: string ) => string };
|
|
|
|
|
|
|
tunnel: Electron agent, production deployment, streaming, Roject integration, chunk animation
— Electron Tunnel Agent app (tray, login, tunnel list, start/stop/delete/create)
— tunnel.rokojori.com deployed (nginx WS upgrade, systemd, certbot, port 3003)
— Streaming relay protocol: res_start/res_data/res_end replaces single-shot response
— Roject: browse-tunnels button in rojo-settings-panel, /tunnels/browse proxy route
— Roject: LLM chat via tunnel (TUNNEL_SERVER_URL, /v1 path, dual-source JWT)
— rojo-chat-panel: typeText() splits large relay chunks for smooth streaming appearance
— Boards, outline, and history updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-16 13:46:51 +00:00
|
|
|
|
async function typeText( text: string, onPiece: ( piece: string ) => void ): Promise<void>
|
|
|
|
|
|
{
|
|
|
|
|
|
const THRESHOLD = 6;
|
|
|
|
|
|
const STEP = 3;
|
|
|
|
|
|
const DELAY_MS = 18;
|
|
|
|
|
|
|
|
|
|
|
|
if ( text.length <= THRESHOLD ) { onPiece( text ); return; }
|
|
|
|
|
|
|
|
|
|
|
|
for ( let i = 0; i < text.length; i += STEP )
|
|
|
|
|
|
{
|
|
|
|
|
|
onPiece( text.slice( i, i + STEP ) );
|
|
|
|
|
|
if ( i + STEP < text.length )
|
|
|
|
|
|
await new Promise<void>( r => setTimeout( r, DELAY_MS ) );
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 18:18:15 +00:00
|
|
|
|
function extractRawText( node: Node ): string
|
|
|
|
|
|
{
|
|
|
|
|
|
if ( node.nodeType === Node.TEXT_NODE ) return node.nodeValue ?? '';
|
|
|
|
|
|
if ( node.nodeName === 'BR' ) return '\n';
|
|
|
|
|
|
if ( node.nodeType !== Node.ELEMENT_NODE ) return '';
|
|
|
|
|
|
let text = '';
|
|
|
|
|
|
node.childNodes.forEach( child => text += extractRawText( child ) );
|
|
|
|
|
|
return text;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 06:44:46 +00:00
|
|
|
|
function formatToolArgs( args: Record<string, unknown> ): string
|
|
|
|
|
|
{
|
|
|
|
|
|
const entries = Object.entries( args );
|
|
|
|
|
|
|
|
|
|
|
|
if ( 0 === entries.length ) return '';
|
|
|
|
|
|
|
|
|
|
|
|
return entries.map( ( [ , v ] ) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if ( typeof v !== 'string' ) return JSON.stringify( v );
|
|
|
|
|
|
const s = v.length > 60 ? v.slice( 0, 57 ) + '…' : v;
|
|
|
|
|
|
return `"${ s }"`;
|
|
|
|
|
|
} ).join( ', ' );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function tryParseResult( raw: string ): unknown
|
|
|
|
|
|
{
|
|
|
|
|
|
try { return JSON.parse( raw ); }
|
|
|
|
|
|
catch { return raw; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatToolResult( parsed: unknown ): string
|
|
|
|
|
|
{
|
|
|
|
|
|
if ( parsed === null || parsed === undefined ) return '✓';
|
|
|
|
|
|
if ( typeof parsed !== 'object' ) return String( parsed );
|
|
|
|
|
|
|
|
|
|
|
|
const obj = parsed as Record<string, unknown>;
|
|
|
|
|
|
|
|
|
|
|
|
if ( true === obj.ok ) return '✓';
|
|
|
|
|
|
|
|
|
|
|
|
if ( Array.isArray( parsed ) ) return `${ parsed.length } items`;
|
|
|
|
|
|
|
|
|
|
|
|
if ( 'content' in obj && typeof obj.content === 'string' )
|
|
|
|
|
|
{
|
|
|
|
|
|
const preview = obj.content.slice( 0, 40 ).replace( /\n/g, ' ' );
|
|
|
|
|
|
return `"${ preview }${ obj.content.length > 40 ? '…' : '' }"`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ( 'type' in obj ) return `${ obj.type } (${ ( obj as any ).size ?? '' })`;
|
|
|
|
|
|
|
|
|
|
|
|
return JSON.stringify( parsed ).slice( 0, 60 );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-16 13:06:33 +00:00
|
|
|
|
interface RojoEntry { id: string; name: string; description: string; path: string; }
|
|
|
|
|
|
|
|
|
|
|
|
function parentDir( filePath: string ): string
|
|
|
|
|
|
{
|
|
|
|
|
|
const slash = filePath.lastIndexOf( '/' );
|
|
|
|
|
|
return slash >= 0 ? filePath.slice( 0, slash ) : 'workspace/rojos';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 18:18:15 +00:00
|
|
|
|
class RojoChatPanel extends HTMLElement
|
|
|
|
|
|
{
|
2026-07-25 20:50:14 +00:00
|
|
|
|
__interfaces__ = [ EditorPanelDefinition.type ];
|
2026-07-16 19:08:27 +00:00
|
|
|
|
_initialized = false;
|
|
|
|
|
|
_conversationId = '';
|
|
|
|
|
|
_sending = false;
|
2026-07-10 18:18:15 +00:00
|
|
|
|
_md: { render: ( s: string ) => string } | null = null;
|
2026-07-16 13:06:33 +00:00
|
|
|
|
_rojos: RojoEntry[] = [];
|
2026-07-16 19:08:27 +00:00
|
|
|
|
_selectedPath = '';
|
|
|
|
|
|
_thinkingInterval: ReturnType<typeof setInterval> | null = null;
|
2026-07-10 18:18:15 +00:00
|
|
|
|
|
|
|
|
|
|
connectedCallback(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
if ( this._initialized ) return;
|
|
|
|
|
|
this._initialized = true;
|
|
|
|
|
|
|
|
|
|
|
|
this._conversationId = crypto.randomUUID();
|
|
|
|
|
|
this._md = markdownit( { html: false, linkify: true, breaks: true } );
|
|
|
|
|
|
|
|
|
|
|
|
this.className = 'rojo-chat-panel';
|
|
|
|
|
|
this.innerHTML = `
|
|
|
|
|
|
<div class="rcp-toolbar">
|
|
|
|
|
|
<div class="rcp-toolbar-icon">🤖</div>
|
2026-07-16 13:06:33 +00:00
|
|
|
|
<select class="rcp-picker" title="Select Rojo">
|
|
|
|
|
|
<option value="">— no rojo selected —</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<button class="rcp-toolbar-btn rcp-new-btn" title="New Rojo">+</button>
|
|
|
|
|
|
<button class="rcp-toolbar-btn rcp-refresh-btn" title="Refresh rojo list">↻</button>
|
2026-07-10 18:18:15 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="rcp-history"></div>
|
|
|
|
|
|
<div class="rcp-input-area">
|
|
|
|
|
|
<div class="rcp-input-text" contenteditable="true"></div>
|
|
|
|
|
|
<div class="rcp-input-buttons">
|
|
|
|
|
|
<button class="rcp-input-btn rcp-plus-btn" title="Attach">+</button>
|
|
|
|
|
|
<button class="rcp-input-btn rcp-send-btn" title="Send">▲</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
const sendBtn = this.querySelector( '.rcp-send-btn' ) as HTMLButtonElement;
|
|
|
|
|
|
const inputText = this.querySelector( '.rcp-input-text' ) as HTMLElement;
|
2026-07-16 13:06:33 +00:00
|
|
|
|
const picker = this.querySelector( '.rcp-picker' ) as HTMLSelectElement;
|
2026-07-10 18:18:15 +00:00
|
|
|
|
|
|
|
|
|
|
sendBtn.addEventListener( 'click', () => this._send() );
|
|
|
|
|
|
|
|
|
|
|
|
inputText.addEventListener( 'keydown', ( e: KeyboardEvent ) =>
|
|
|
|
|
|
{
|
2026-07-16 13:06:33 +00:00
|
|
|
|
if ( e.key === 'Enter' && !e.shiftKey ) { e.preventDefault(); this._send(); }
|
2026-07-10 18:18:15 +00:00
|
|
|
|
} );
|
2026-07-16 13:06:33 +00:00
|
|
|
|
|
2026-07-16 19:08:27 +00:00
|
|
|
|
inputText.addEventListener( 'focus', () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
setTimeout( () => inputText.scrollIntoView( { behavior: 'smooth', block: 'nearest' } ), 300 );
|
|
|
|
|
|
} );
|
|
|
|
|
|
|
2026-07-16 13:06:33 +00:00
|
|
|
|
picker.addEventListener( 'change', () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
this._selectedPath = picker.value;
|
|
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
|
|
this.querySelector( '.rcp-new-btn' )!.addEventListener( 'click', () => this._createRojo() );
|
|
|
|
|
|
this.querySelector( '.rcp-refresh-btn' )!.addEventListener( 'click', () => this._loadRojos() );
|
|
|
|
|
|
|
|
|
|
|
|
this._loadRojos();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async _loadRojos(): Promise<void>
|
|
|
|
|
|
{
|
history: Saturday, 2 August 2026 — Claude API + Claude Code providers for rojo-chat-panel
- Claude API provider (@anthropic-ai/sdk): streaming, separate session history,
tool use support, rojo-settings-panel claude endpoint type
- Claude Code subprocess provider: --print --output-format stream-json --verbose,
--session-id/--resume multi-turn, shell:false Windows fix,
rojo-settings-panel claude-code endpoint type (no API key)
- Rojo local-mode fixes: detection, creation, system-prompt delivery
- Three subprocess bugs fixed: missing --verbose, stdin blocking, cmd.exe mangling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-02 10:32:11 +00:00
|
|
|
|
const editor = Editor.get();
|
|
|
|
|
|
let url: string;
|
|
|
|
|
|
|
|
|
|
|
|
if ( editor.localRoot )
|
|
|
|
|
|
{
|
|
|
|
|
|
url = `/api/local/rojos?root=${ encodeURIComponent( editor.localRoot ) }`;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if ( editor.remoteProject )
|
|
|
|
|
|
{
|
|
|
|
|
|
url = `/api/remote/rojos/${ editor.remoteProject }/list`;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if ( editor.projectId )
|
|
|
|
|
|
{
|
|
|
|
|
|
url = `/api/rojos/${ editor.projectId }/list`;
|
|
|
|
|
|
}
|
|
|
|
|
|
else { return; }
|
2026-07-16 13:06:33 +00:00
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
history: Saturday, 2 August 2026 — Claude API + Claude Code providers for rojo-chat-panel
- Claude API provider (@anthropic-ai/sdk): streaming, separate session history,
tool use support, rojo-settings-panel claude endpoint type
- Claude Code subprocess provider: --print --output-format stream-json --verbose,
--session-id/--resume multi-turn, shell:false Windows fix,
rojo-settings-panel claude-code endpoint type (no API key)
- Rojo local-mode fixes: detection, creation, system-prompt delivery
- Three subprocess bugs fixed: missing --verbose, stdin blocking, cmd.exe mangling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-02 10:32:11 +00:00
|
|
|
|
const res = await fetch( url );
|
2026-07-16 13:06:33 +00:00
|
|
|
|
if ( !res.ok ) return;
|
|
|
|
|
|
this._rojos = await res.json() as RojoEntry[];
|
|
|
|
|
|
this._renderPicker();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch {}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_renderPicker(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
const picker = this.querySelector( '.rcp-picker' ) as HTMLSelectElement;
|
|
|
|
|
|
const prev = this._selectedPath;
|
|
|
|
|
|
|
|
|
|
|
|
picker.innerHTML = '<option value="">— no rojo selected —</option>' +
|
|
|
|
|
|
this._rojos.map( r =>
|
|
|
|
|
|
`<option value="${ r.path }">${ r.name || r.path.slice( r.path.lastIndexOf( '/' ) + 1 ) }</option>`
|
|
|
|
|
|
).join( '' );
|
|
|
|
|
|
|
|
|
|
|
|
if ( prev && this._rojos.some( r => r.path === prev ) )
|
|
|
|
|
|
{
|
|
|
|
|
|
picker.value = prev;
|
|
|
|
|
|
this._selectedPath = prev;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
this._selectedPath = '';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async _createRojo(): Promise<void>
|
|
|
|
|
|
{
|
history: Saturday, 2 August 2026 — Claude API + Claude Code providers for rojo-chat-panel
- Claude API provider (@anthropic-ai/sdk): streaming, separate session history,
tool use support, rojo-settings-panel claude endpoint type
- Claude Code subprocess provider: --print --output-format stream-json --verbose,
--session-id/--resume multi-turn, shell:false Windows fix,
rojo-settings-panel claude-code endpoint type (no API key)
- Rojo local-mode fixes: detection, creation, system-prompt delivery
- Three subprocess bugs fixed: missing --verbose, stdin blocking, cmd.exe mangling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-02 10:32:11 +00:00
|
|
|
|
const editor = Editor.get();
|
2026-07-16 13:06:33 +00:00
|
|
|
|
|
|
|
|
|
|
const dir = this._selectedPath
|
|
|
|
|
|
? parentDir( this._selectedPath )
|
|
|
|
|
|
: 'workspace/rojos';
|
|
|
|
|
|
|
history: Saturday, 2 August 2026 — Claude API + Claude Code providers for rojo-chat-panel
- Claude API provider (@anthropic-ai/sdk): streaming, separate session history,
tool use support, rojo-settings-panel claude endpoint type
- Claude Code subprocess provider: --print --output-format stream-json --verbose,
--session-id/--resume multi-turn, shell:false Windows fix,
rojo-settings-panel claude-code endpoint type (no API key)
- Rojo local-mode fixes: detection, creation, system-prompt delivery
- Three subprocess bugs fixed: missing --verbose, stdin blocking, cmd.exe mangling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-02 10:32:11 +00:00
|
|
|
|
let url: string;
|
|
|
|
|
|
let body: Record<string, string>;
|
|
|
|
|
|
|
|
|
|
|
|
if ( editor.localRoot )
|
|
|
|
|
|
{
|
|
|
|
|
|
url = '/api/local/rojos/create';
|
|
|
|
|
|
body = { root: editor.localRoot, parentDir: dir };
|
|
|
|
|
|
}
|
|
|
|
|
|
else if ( editor.remoteProject )
|
|
|
|
|
|
{
|
|
|
|
|
|
url = `/api/remote/rojos/${ editor.remoteProject }/create`;
|
|
|
|
|
|
body = { parentDir: dir };
|
|
|
|
|
|
}
|
|
|
|
|
|
else if ( editor.projectId )
|
|
|
|
|
|
{
|
|
|
|
|
|
url = `/api/rojos/${ editor.projectId }/create`;
|
|
|
|
|
|
body = { parentDir: dir };
|
|
|
|
|
|
}
|
|
|
|
|
|
else { return; }
|
|
|
|
|
|
|
2026-07-16 13:06:33 +00:00
|
|
|
|
try
|
|
|
|
|
|
{
|
history: Saturday, 2 August 2026 — Claude API + Claude Code providers for rojo-chat-panel
- Claude API provider (@anthropic-ai/sdk): streaming, separate session history,
tool use support, rojo-settings-panel claude endpoint type
- Claude Code subprocess provider: --print --output-format stream-json --verbose,
--session-id/--resume multi-turn, shell:false Windows fix,
rojo-settings-panel claude-code endpoint type (no API key)
- Rojo local-mode fixes: detection, creation, system-prompt delivery
- Three subprocess bugs fixed: missing --verbose, stdin blocking, cmd.exe mangling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-02 10:32:11 +00:00
|
|
|
|
const res = await fetch( url,
|
2026-07-16 13:06:33 +00:00
|
|
|
|
{
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
history: Saturday, 2 August 2026 — Claude API + Claude Code providers for rojo-chat-panel
- Claude API provider (@anthropic-ai/sdk): streaming, separate session history,
tool use support, rojo-settings-panel claude endpoint type
- Claude Code subprocess provider: --print --output-format stream-json --verbose,
--session-id/--resume multi-turn, shell:false Windows fix,
rojo-settings-panel claude-code endpoint type (no API key)
- Rojo local-mode fixes: detection, creation, system-prompt delivery
- Three subprocess bugs fixed: missing --verbose, stdin blocking, cmd.exe mangling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-02 10:32:11 +00:00
|
|
|
|
body: JSON.stringify( body ),
|
2026-07-16 13:06:33 +00:00
|
|
|
|
} );
|
|
|
|
|
|
|
|
|
|
|
|
if ( !res.ok ) return;
|
|
|
|
|
|
const { path: newPath } = await res.json() as { id: string; path: string; name: string };
|
|
|
|
|
|
|
|
|
|
|
|
await this._loadRojos();
|
|
|
|
|
|
this._selectedPath = newPath;
|
|
|
|
|
|
( this.querySelector( '.rcp-picker' ) as HTMLSelectElement ).value = newPath;
|
|
|
|
|
|
|
history: Saturday, 2 August 2026 — Claude API + Claude Code providers for rojo-chat-panel
- Claude API provider (@anthropic-ai/sdk): streaming, separate session history,
tool use support, rojo-settings-panel claude endpoint type
- Claude Code subprocess provider: --print --output-format stream-json --verbose,
--session-id/--resume multi-turn, shell:false Windows fix,
rojo-settings-panel claude-code endpoint type (no API key)
- Rojo local-mode fixes: detection, creation, system-prompt delivery
- Three subprocess bugs fixed: missing --verbose, stdin blocking, cmd.exe mangling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-02 10:32:11 +00:00
|
|
|
|
await editor.openDocument( newPath );
|
2026-07-16 13:06:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
catch {}
|
2026-07-10 18:18:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-16 19:08:27 +00:00
|
|
|
|
_startThinking( bubble: HTMLElement ): void
|
|
|
|
|
|
{
|
2026-07-16 19:19:59 +00:00
|
|
|
|
const frames = [ '.', '..', '...', '....', '---', '--', "/","|",":","·" ];
|
2026-07-16 19:08:27 +00:00
|
|
|
|
let i = 0;
|
|
|
|
|
|
bubble.textContent = frames[ 0 ];
|
|
|
|
|
|
this._thinkingInterval = setInterval( () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
i = ( i + 1 ) % frames.length;
|
|
|
|
|
|
bubble.textContent = frames[ i ];
|
|
|
|
|
|
}, 250 );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_stopThinking(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
if ( this._thinkingInterval === null ) return;
|
|
|
|
|
|
clearInterval( this._thinkingInterval );
|
|
|
|
|
|
this._thinkingInterval = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 18:18:15 +00:00
|
|
|
|
async _send(): Promise<void>
|
|
|
|
|
|
{
|
|
|
|
|
|
if ( this._sending ) return;
|
|
|
|
|
|
|
2026-07-16 13:06:33 +00:00
|
|
|
|
const inputText = this.querySelector( '.rcp-input-text' ) as HTMLElement;
|
|
|
|
|
|
const sendBtn = this.querySelector( '.rcp-send-btn' ) as HTMLButtonElement;
|
|
|
|
|
|
const history = this.querySelector( '.rcp-history' ) as HTMLElement;
|
2026-07-10 18:18:15 +00:00
|
|
|
|
|
|
|
|
|
|
const text = extractRawText( inputText ).replaceAll( ' ', ' ' ).trim();
|
|
|
|
|
|
if ( !text ) return;
|
|
|
|
|
|
|
|
|
|
|
|
inputText.innerHTML = '';
|
|
|
|
|
|
this._sending = true;
|
|
|
|
|
|
sendBtn.disabled = true;
|
|
|
|
|
|
|
2026-07-16 13:06:33 +00:00
|
|
|
|
const userBubble = document.createElement( 'div' );
|
|
|
|
|
|
userBubble.className = 'rcp-user-bubble';
|
2026-07-10 18:18:15 +00:00
|
|
|
|
userBubble.textContent = text;
|
|
|
|
|
|
history.appendChild( userBubble );
|
|
|
|
|
|
history.scrollTop = history.scrollHeight;
|
|
|
|
|
|
|
|
|
|
|
|
const assistantBubble = document.createElement( 'div' );
|
|
|
|
|
|
assistantBubble.className = 'rcp-assistant-bubble';
|
|
|
|
|
|
history.appendChild( assistantBubble );
|
|
|
|
|
|
history.scrollTop = history.scrollHeight;
|
|
|
|
|
|
|
2026-07-16 19:08:27 +00:00
|
|
|
|
this._startThinking( assistantBubble );
|
|
|
|
|
|
|
2026-07-10 18:18:15 +00:00
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-07-16 13:06:33 +00:00
|
|
|
|
const body: Record<string, string> = { id: this._conversationId, message: text };
|
|
|
|
|
|
|
|
|
|
|
|
if ( this._selectedPath )
|
|
|
|
|
|
{
|
history: Saturday, 2 August 2026 — Claude API + Claude Code providers for rojo-chat-panel
- Claude API provider (@anthropic-ai/sdk): streaming, separate session history,
tool use support, rojo-settings-panel claude endpoint type
- Claude Code subprocess provider: --print --output-format stream-json --verbose,
--session-id/--resume multi-turn, shell:false Windows fix,
rojo-settings-panel claude-code endpoint type (no API key)
- Rojo local-mode fixes: detection, creation, system-prompt delivery
- Three subprocess bugs fixed: missing --verbose, stdin blocking, cmd.exe mangling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-02 10:32:11 +00:00
|
|
|
|
const editor = Editor.get();
|
|
|
|
|
|
body.rojoPath = this._selectedPath;
|
|
|
|
|
|
|
|
|
|
|
|
if ( editor.localRoot ) { body.localRoot = editor.localRoot; }
|
|
|
|
|
|
else if ( editor.remoteProject ) { body.remoteProject = editor.remoteProject; }
|
|
|
|
|
|
else { body.projectId = editor.projectId; }
|
2026-07-16 13:06:33 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-10 18:18:15 +00:00
|
|
|
|
const response = await fetch( '/api/rojos/chat',
|
2026-07-16 13:06:33 +00:00
|
|
|
|
{
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify( body ),
|
|
|
|
|
|
} );
|
2026-07-10 18:18:15 +00:00
|
|
|
|
|
|
|
|
|
|
if ( !response.body ) throw new Error( 'No response body' );
|
|
|
|
|
|
|
|
|
|
|
|
const reader = response.body.getReader();
|
|
|
|
|
|
const decoder = new TextDecoder();
|
|
|
|
|
|
let markdown = '';
|
2026-07-30 06:44:46 +00:00
|
|
|
|
let pendingToolBubble: HTMLElement | null = null;
|
2026-07-10 18:18:15 +00:00
|
|
|
|
|
|
|
|
|
|
while ( true )
|
|
|
|
|
|
{
|
|
|
|
|
|
const { done, value } = await reader.read();
|
|
|
|
|
|
if ( done ) break;
|
|
|
|
|
|
|
|
|
|
|
|
const raw = decoder.decode( value, { stream: true } );
|
|
|
|
|
|
|
|
|
|
|
|
for ( const line of raw.split( '\n' ) )
|
|
|
|
|
|
{
|
|
|
|
|
|
const trimmed = line.trim();
|
|
|
|
|
|
if ( !trimmed ) continue;
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
2026-07-30 06:44:46 +00:00
|
|
|
|
const msg = JSON.parse( trimmed ) as
|
|
|
|
|
|
{
|
|
|
|
|
|
type: string;
|
|
|
|
|
|
text?: string;
|
|
|
|
|
|
name?: string;
|
|
|
|
|
|
args?: Record<string, unknown>;
|
|
|
|
|
|
result?: string;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if ( 'CHAT' === msg.type && msg.text )
|
2026-07-10 18:18:15 +00:00
|
|
|
|
{
|
2026-07-16 19:08:27 +00:00
|
|
|
|
if ( !markdown )
|
|
|
|
|
|
{
|
|
|
|
|
|
this._stopThinking();
|
|
|
|
|
|
assistantBubble.innerHTML = '';
|
|
|
|
|
|
}
|
tunnel: Electron agent, production deployment, streaming, Roject integration, chunk animation
— Electron Tunnel Agent app (tray, login, tunnel list, start/stop/delete/create)
— tunnel.rokojori.com deployed (nginx WS upgrade, systemd, certbot, port 3003)
— Streaming relay protocol: res_start/res_data/res_end replaces single-shot response
— Roject: browse-tunnels button in rojo-settings-panel, /tunnels/browse proxy route
— Roject: LLM chat via tunnel (TUNNEL_SERVER_URL, /v1 path, dual-source JWT)
— rojo-chat-panel: typeText() splits large relay chunks for smooth streaming appearance
— Boards, outline, and history updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-16 13:46:51 +00:00
|
|
|
|
await typeText( msg.text, piece =>
|
|
|
|
|
|
{
|
|
|
|
|
|
markdown += piece;
|
|
|
|
|
|
assistantBubble.innerHTML = this._md!.render( markdown );
|
|
|
|
|
|
history.scrollTop = history.scrollHeight;
|
|
|
|
|
|
} );
|
2026-07-10 18:18:15 +00:00
|
|
|
|
}
|
2026-07-30 06:44:46 +00:00
|
|
|
|
else if ( 'TOOL_CALL' === msg.type && msg.name )
|
|
|
|
|
|
{
|
|
|
|
|
|
this._stopThinking();
|
|
|
|
|
|
pendingToolBubble = document.createElement( 'div' );
|
|
|
|
|
|
pendingToolBubble.className = 'rcp-tool-activity';
|
|
|
|
|
|
pendingToolBubble.innerHTML =
|
|
|
|
|
|
`<rcp-tool-call-line class="rcp-tool-call-line">` +
|
|
|
|
|
|
`⚙ <span class="rcp-tool-call-name">${ msg.name }</span>` +
|
|
|
|
|
|
` ${ formatToolArgs( msg.args ?? {} ) }` +
|
|
|
|
|
|
`</rcp-tool-call-line>`;
|
|
|
|
|
|
history.insertBefore( pendingToolBubble, assistantBubble );
|
|
|
|
|
|
history.scrollTop = history.scrollHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if ( 'TOOL_RESULT' === msg.type && pendingToolBubble )
|
|
|
|
|
|
{
|
|
|
|
|
|
const parsed = tryParseResult( msg.result ?? '' );
|
|
|
|
|
|
const isError = parsed && typeof parsed === 'object' && 'error' in parsed;
|
|
|
|
|
|
const label = isError ? `✗ ${ ( parsed as any ).error }` : formatToolResult( parsed );
|
|
|
|
|
|
const resultLine = document.createElement( 'rcp-tool-result-line' );
|
|
|
|
|
|
resultLine.className = `rcp-tool-result-line${ isError ? ' error' : '' }`;
|
|
|
|
|
|
resultLine.textContent = `→ ${ label }`;
|
|
|
|
|
|
pendingToolBubble.appendChild( resultLine );
|
|
|
|
|
|
pendingToolBubble = null;
|
|
|
|
|
|
history.scrollTop = history.scrollHeight;
|
|
|
|
|
|
}
|
2026-07-10 18:18:15 +00:00
|
|
|
|
}
|
2026-07-16 13:06:33 +00:00
|
|
|
|
catch {}
|
2026-07-10 18:18:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-16 19:08:27 +00:00
|
|
|
|
if ( !markdown ) { this._stopThinking(); assistantBubble.textContent = '(no response)'; }
|
2026-07-10 18:18:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
catch ( err )
|
|
|
|
|
|
{
|
2026-07-16 19:08:27 +00:00
|
|
|
|
this._stopThinking();
|
2026-07-10 18:18:15 +00:00
|
|
|
|
assistantBubble.textContent = '(error: could not reach Rojo)';
|
|
|
|
|
|
console.error( err );
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
this._sending = false;
|
|
|
|
|
|
sendBtn.disabled = false;
|
|
|
|
|
|
history.scrollTop = history.scrollHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
addContextMenuEntries( dir: ContextMenuDirectory ): void
|
|
|
|
|
|
{
|
2026-07-16 13:06:33 +00:00
|
|
|
|
const rojo = this._rojos.find( r => r.path === this._selectedPath );
|
|
|
|
|
|
dir.add( new ContextMenuReadOnlyEntry( dir, `Rojo: ${ rojo?.name ?? 'none' }` ) );
|
2026-07-10 18:18:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
customElements.define( 'rojo-chat-panel', RojoChatPanel );
|