feat(remotes): full remote walkthrough, complete local remotes with editable functions
This commit is contained in:
1 parent
5009a4eb4c
commit
da1420d04b
4 files changed
+206
-48
No files matched your search
+171
-25
@@ -99,14 +99,43 @@ const STORAGE_KEY = 'local_remotes';
|
||||
let connectListenerCancel = null;
|
||||
let disconnectListenerCancel = null;
|
||||
|
||||
// Listen for remote-call requests destined for local remotes.
|
||||
// Persists for the session, not just while the tab is visible.
|
||||
// TODO: restore after disconnections
|
||||
let callListenerCancel = null;
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchRemotes();
|
||||
let connectListenerCancel = backend.onAction('remote-connected', ev => {
|
||||
remotes.value.push(ev.data.remote);
|
||||
});
|
||||
let disconnectListenerCancel = backend.onAction('remote-disconnected', ev => {
|
||||
// TODO
|
||||
remotes.value = remotes.value.filter((r) => r.name != ev.data.remotename);
|
||||
});
|
||||
|
||||
if (!callListenerCancel) {
|
||||
callListenerCancel = backend.onAction('remote-call', async ({ requestId, remotename, function: fnName, args }) => {
|
||||
console.log(`Received remote call for: ${remotename}, ${fnName}`);
|
||||
const local = localRemoteList.value.find(r => r.name == remotename && r.connected);
|
||||
if (!local) return;
|
||||
const funcDef = local.functions.find(f => f.name == fnName);
|
||||
if (!funcDef) {
|
||||
await backend.sendAction('remote-call-result', { requestId, error: `Function "${fnName}" not found on local remote` });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// TODO: Upgrade from eval to a properly scoped Function constructor or sandboxes
|
||||
const fnBody = funcDef.body || 'return {};';
|
||||
const scopeKeys = Object.keys(args);
|
||||
const scopeValues = scopeKeys.map(k => args[k]);
|
||||
const createFunc = new Function(...scopeKeys, fnBody);
|
||||
const result = await createFunc(...scopeValues);
|
||||
await backend.sendAction('remote-call-result', { requestId, result });
|
||||
} catch (e) {
|
||||
await backend.sendAction('remote-call-result', { requestId, error: String(e) });
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(async () => {
|
||||
@@ -176,17 +205,17 @@ async function doCall() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
callResult.value = await backend.sendAction('remote-call', {
|
||||
callResult.value = (await backend.sendAction('remote-call', {
|
||||
remotename: activeRemoteName.value,
|
||||
function: activeFunction.value.name,
|
||||
args: argsParsed,
|
||||
});
|
||||
})).result;
|
||||
} catch (e) {
|
||||
callError.value = e.message || String(e);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Local Remotes (unchanged from previous iteration) ----
|
||||
// ---- Local Remotes ----
|
||||
function loadLocalRemoteList() {
|
||||
let data = [];
|
||||
try {
|
||||
@@ -196,7 +225,10 @@ function loadLocalRemoteList() {
|
||||
if (Array.isArray(parsed)) data = parsed;
|
||||
}
|
||||
} catch (e) {}
|
||||
data.forEach(r => r.connected = false);
|
||||
data.forEach(r => {
|
||||
r.connected = false
|
||||
r.functions = r.functions || [];
|
||||
});
|
||||
localRemoteList.value = data;
|
||||
}
|
||||
|
||||
@@ -213,10 +245,20 @@ async function connectLocalRemote(remote) {
|
||||
ui.alert("Cannot connect remote with empty name");
|
||||
return;
|
||||
}
|
||||
|
||||
let result = await backend.sendAction('remote-connect', {
|
||||
remotename: remote.name,
|
||||
functions: remote.functions,
|
||||
functions: remote.functions?.map(f => ({
|
||||
name: f.name,
|
||||
description: '',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: f.arguments.reduce((acc, arg) => {
|
||||
acc[arg.name] = { type: arg.type, description: '' };
|
||||
return acc;
|
||||
}, {}),
|
||||
required: f.arguments.map(a => a.name)
|
||||
}
|
||||
})) || [],
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
@@ -247,7 +289,43 @@ function removeLocalRemote(remote) {
|
||||
saveLocalRemote();
|
||||
}
|
||||
|
||||
function addFunctionToRemote(remote) {
|
||||
if (!remote.functions) remote.functions = [];
|
||||
remote.functions.push({
|
||||
name: `function_${Date.now()}`,
|
||||
arguments: [{ name: 'arg0', type: 'string' }],
|
||||
body: '// Return your result object\nreturn {};\n'
|
||||
});
|
||||
saveLocalRemote();
|
||||
}
|
||||
|
||||
function removeFunctionFromRemote(remote, index) {
|
||||
remote.functions.splice(index, 1);
|
||||
saveLocalRemote();
|
||||
}
|
||||
|
||||
function addArgument(fn) {
|
||||
const idx = fn.arguments.length;
|
||||
fn.arguments.push({ name: `arg${idx}`, type: 'string' });
|
||||
saveLocalRemote();
|
||||
}
|
||||
|
||||
function removeArgument(fn, argIdx) {
|
||||
fn.arguments.splice(argIdx, 1);
|
||||
saveLocalRemote();
|
||||
}
|
||||
|
||||
loadLocalRemoteList();
|
||||
|
||||
// ---- UI State and Helpers ---- //
|
||||
const fnExpandedSet = ref(new Set());
|
||||
|
||||
function toggleFnExpanded(index, fIdx) {
|
||||
const key = `${index}-${fIdx}`;
|
||||
const newSet = new Set(fnExpandedSet.value);
|
||||
if (newSet.has(key)) newSet.delete(key); else newSet.add(key);
|
||||
fnExpandedSet.value = newSet;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -334,23 +412,80 @@ loadLocalRemoteList();
|
||||
<!-- Local Remotes Section -->
|
||||
<!-- ========================== -->
|
||||
<section class="section">
|
||||
<h2>Local Remotes</h2>
|
||||
<div class="section-header">
|
||||
<h2>Local Remotes</h2>
|
||||
</div>
|
||||
<div class="remote-list">
|
||||
<div v-for="(remote, index) in localRemoteList" :key="index" class="remote-entry">
|
||||
<div v-if="remote.connected" class="view-mode">
|
||||
<span class="status-text">Connected as: {{ remote.name }}</span>
|
||||
<button class="btn btn-secondary" @click="disconnectLocalRemote(remote)">Disconnect</button>
|
||||
<div v-for="(remote, index) in localRemoteList" :key="index" class="card">
|
||||
<!-- Name / Connect bar -->
|
||||
<div class="card-header">
|
||||
<div class="view-mode" style="display:flex;align-items:center;gap:0.75rem;">
|
||||
<input
|
||||
v-if="!remote.connected"
|
||||
type="text"
|
||||
v-model="remote.name"
|
||||
placeholder="Enter remote name..."
|
||||
class="input-field"
|
||||
/>
|
||||
<span v-else><strong>{{ remote.name }}</strong></span>
|
||||
<template v-if="!remote.connected">
|
||||
<button class="btn btn-primary" @click="connectLocalRemote(remote)">Connect</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="status-dot active"></span>
|
||||
<button class="btn btn-secondary" @click="disconnectLocalRemote(remote)">Disconnect</button>
|
||||
</template>
|
||||
<button class="btn btn-danger" @click="removeLocalRemote(remote)">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="view-mode">
|
||||
<input
|
||||
type="text"
|
||||
v-model="remote.name"
|
||||
placeholder="Enter remote name..."
|
||||
class="input-field"
|
||||
/>
|
||||
<button class="btn btn-primary" @click="connectLocalRemote(remote)">Connect</button>
|
||||
|
||||
<!-- Expanded: function definitions -->
|
||||
<div class="card-body">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem;">
|
||||
<strong>Functions</strong>
|
||||
<button class="btn btn-primary" @click="addFunctionToRemote(remote)">+ Add Function</button>
|
||||
</div>
|
||||
<div v-if="!remote.functions || remote.functions.length === 0" style="color:var(--color-text-mute);font-size:0.85em;">No functions yet.</div>
|
||||
<div v-for="(fn, fIdx) in remote.functions" :key="fIdx" class="func-def-block">
|
||||
<div class="func-def-header">
|
||||
<strong>{{ fn.name }}</strong>
|
||||
<button class="btn btn-danger btn-sm" @click="removeFunctionFromRemote(remote, fIdx)">Remove</button>
|
||||
<span style="cursor:pointer;" @click.stop="toggleFnExpanded(index, fIdx)">
|
||||
{{ fnExpandedSet.has(`${index}-${fIdx}`) ? 'collapse' : 'expand' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Arguments table -->
|
||||
<div class="func-def-row">
|
||||
<div class="args-table">
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Type</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="(arg, aIdx) in fn.arguments" :key="aIdx">
|
||||
<td><input type="text" v-model="arg.name" class="input-small" :disabled="remote.connected" /></td>
|
||||
<td>
|
||||
<select v-model="arg.type" class="input-small" :disabled="remote.connected">
|
||||
<option value="string">string</option>
|
||||
<option value="number">number</option>
|
||||
<option value="boolean">boolean</option>
|
||||
<option value="object">object</option>
|
||||
<option value="array">array</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><button class="btn btn-danger btn-xs" @click="removeArgument(fn, aIdx)">×</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="btn btn-secondary btn-xs" @click="addArgument(fn)" :disabled="remote.connected">+ Arg</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JS body -->
|
||||
<div v-if="fnExpandedSet.has(`${index}-${fIdx}`)" class="func-def-row">
|
||||
<textarea v-model="fn.body" rows="4" placeholder="return {};" spellcheck="false" class="js-editor" :readonly="remote.connected"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-danger" @click="removeLocalRemote(remote)">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-primary" @click="createLocalRemote">+ Create Local Remote</button>
|
||||
@@ -390,9 +525,8 @@ loadLocalRemoteList();
|
||||
.call-inputs label { display: block; font-weight: bold; margin-bottom: 0.25rem; }
|
||||
.call-inputs textarea { width: 100%; padding: 0.5rem; border: 1px solid var(--color-border); border-radius: var(--border-radius); resize: vertical; font-family: monospace; font-size: 0.85em; margin-bottom: 0.5rem; }
|
||||
.output-block { margin-bottom: 0.75rem; }
|
||||
.output-block.success pre { background: var(--color-success-bg, #dcfce7); padding: 0.5rem; border-radius: var(--border-radius); overflow-x: auto; }
|
||||
.output-block.error { color: #ef4444; }
|
||||
.output-block.error pre { background: var(--color-error-bg, #fee2e2); padding: 0.5rem; border-radius: var(--border-radius); overflow-x: auto; }
|
||||
.output-block.success pre { background: var(--color-success-bg, #3c5c47); padding: 0.5rem; border-radius: var(--border-radius); overflow-x: auto; }
|
||||
.output-block.error pre { background: var(--color-error-bg, #5e4242); padding: 0.5rem; border-radius: var(--border-radius); overflow-x: auto; }
|
||||
.output-placeholder { padding: 1em; color: var(--color-text-mute); font-style: italic; }
|
||||
|
||||
/* ---------- Buttons (shared across sections) ---------- */
|
||||
@@ -401,10 +535,22 @@ loadLocalRemoteList();
|
||||
.btn-secondary { background: var(--color-muted, #9ca3af); color: #fff; }
|
||||
.btn-danger { background: var(--color-error, #ef4444); color: #fff; }
|
||||
|
||||
/* ---------- Local remotes (preserved from original) ---------- */
|
||||
/* ---------- Local remotes ---------- */
|
||||
.remote-list { margin-top: 1rem; }
|
||||
.remote-entry { display: flex; align-items: center; gap: 1rem; padding: 1rem; border: 1px solid var(--color-border); border-radius: var(--border-radius); margin-bottom: 1rem; background-color: var(--color-background-soft); }
|
||||
.view-mode { display: flex; align-items: center; gap: 1rem; flex-grow: 1; }
|
||||
.status-text { font-weight: bold; }
|
||||
.input-field { flex-grow: 1; padding: 0.5rem; border: 1px solid var(--color-border); border-radius: var(--border-radius); }
|
||||
.func-def-block { border: 1px solid var(--color-border); border-radius: var(--border-radius); margin-bottom: 0.5rem; padding: 0.5rem; background: var(--color-background); }
|
||||
.func-def-header { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.35rem; font-family: monospace; }
|
||||
.func-def-row { margin-bottom: 0.25rem; }
|
||||
.args-table table { width: 100%; border-collapse: collapse; font-size: 0.85em; }
|
||||
.args-table th { text-align: left; color: var(--color-text-mute); padding: 0.2em 0.3em; border-bottom: 1px solid var(--color-border); }
|
||||
.args-table td { padding: 0.2em 0.3em; }
|
||||
.args-table input[type="text"] { width: 6em; padding: 0.2em 0.4em; border: 1px solid var(--color-border); border-radius: 4px; font-size: 0.85em; }
|
||||
.args-table select { padding: 0.2em 0.4em; border: 1px solid var(--color-border); border-radius: 4px; font-size: 0.85em; }
|
||||
.js-editor { width: 100%; min-height: 3em; max-height: 30em; max-width: 100%; resize: both; padding: 0.5rem; border: 1px solid var(--color-border); border-radius: var(--border-radius); font-family: monospace; font-size: 0.85em; box-sizing: border-box; }
|
||||
.btn-sm { padding: 0.25em 0.6em; font-size: 0.8em; }
|
||||
.btn-xs { padding: 0.15em 0.4em; font-size: 0.75em; }
|
||||
.input-small { padding: 0.2em 0.4em; border: 1px solid var(--color-border); border-radius: 4px; font-size: 0.85em; }
|
||||
</style>
|
||||
Reference in new issue
Block a user