feat(chat): support chat resources, implement chat-based remote discovery via resource messages
This commit is contained in:
1 parent
da1420d04b
commit
30afcf2803
10 files changed
+142
-85
No files matched your search
@@ -32,6 +32,8 @@ function chatEntryClass(msg) {
|
||||
return ['note'];
|
||||
} else if (msg.type == 'message') {
|
||||
return ['message', msg.sender == 'user' ? 'sent' : 'received'];
|
||||
} else if (msg.type == 'resource') {
|
||||
return ['resource', 'resource-' + (msg.subkind || 'generic')];
|
||||
} else if (msg.type == 'log') {
|
||||
return ['message', 'log'];
|
||||
} else {
|
||||
@@ -39,6 +41,11 @@ function chatEntryClass(msg) {
|
||||
}
|
||||
}
|
||||
|
||||
function resourceLabel(msg) {
|
||||
if (msg.subkind == 'remote') return 'Remote';
|
||||
return msg.subkind || 'Resource';
|
||||
}
|
||||
|
||||
const format = ref(null);
|
||||
const content = ref("");
|
||||
|
||||
@@ -79,6 +86,11 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
|
||||
<div class="content">{{content}}</div>
|
||||
</template>
|
||||
|
||||
<template v-if="message.type == 'resource'">
|
||||
<div class="resource-label">{{ resourceLabel(message) }}</div>
|
||||
<div class="content">{{content}}</div>
|
||||
</template>
|
||||
|
||||
<template v-if="message.type == 'log'">
|
||||
<div class="sender">Log ({{message.sender}})</div>
|
||||
<div class="content">{{content}}</div>
|
||||
@@ -143,4 +155,26 @@ watch(() => [props.message.format, props.message.content], ([newFormat, newConte
|
||||
font-family: monospace;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
.resource {
|
||||
align-self: center;
|
||||
width: fit-content;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
border: 1px dashed var(--color-border);
|
||||
background-color: var(--color-background-soft);
|
||||
font-size: 85%;
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.resource .resource-label {
|
||||
font-weight: bold;
|
||||
opacity: 60%;
|
||||
}
|
||||
|
||||
.resource .content {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
@@ -84,6 +84,7 @@
|
||||
for (let entry of messages.value) {
|
||||
if (entry.id == it.id) {
|
||||
entry.type = it.kind;
|
||||
entry.subkind = it.subkind;
|
||||
entry.format = it.format;
|
||||
entry.content = it.content;
|
||||
entry.threadId = it.threadId;
|
||||
@@ -109,6 +110,7 @@
|
||||
addMessage({
|
||||
id: it.id,
|
||||
type: it.kind,
|
||||
subkind: it.subkind,
|
||||
format: it.format,
|
||||
sender: it.sender,
|
||||
content: it.content,
|
||||
@@ -124,6 +126,7 @@
|
||||
//id: it.id,
|
||||
id: _messageNonce++,
|
||||
type: it.kind,
|
||||
subkind: it.subkind,
|
||||
sender: it.sender,
|
||||
content: it.content,
|
||||
threadId: it.threadId,
|
||||
@@ -296,8 +299,8 @@
|
||||
// New message object with updated name
|
||||
const message = Object.assign({}, _msg, {sender: senderName(_msg.sender)});
|
||||
|
||||
// Non-threaded messages and notes go directly to the entry list
|
||||
if (message.threadId == null || message.type == 'note') {
|
||||
// Non-threaded messages, notes, and resources go directly to the entry list
|
||||
if (message.threadId == null) {
|
||||
entries.push(message);
|
||||
|
||||
} else {
|
||||
@@ -343,6 +346,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function addRemoteResource () {
|
||||
const res = await backend.sendAction('remote-list');
|
||||
const remotes = res.remotes || [];
|
||||
if (remotes.length == 0) {
|
||||
ui.alert("No remotes connected");
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = await ui.contextMenu(remotes.map(r => ({
|
||||
value: r.name,
|
||||
title: r.name + (r.description ? ` — ${r.description}` : ''),
|
||||
})));
|
||||
if (!selected) return;
|
||||
|
||||
const threadId = currentThreadId.value;
|
||||
backend.sendAction("send-chat-message", {
|
||||
chatId, content: selected, threadId,
|
||||
kind: 'resource', subkind: 'remote',
|
||||
}).then((response) => {
|
||||
if (response['error']) {
|
||||
ui.alert("Error adding remote: " + response['error']);
|
||||
}
|
||||
});
|
||||
|
||||
// Optimistic entry, matched against the server broadcast by content
|
||||
addMessage({
|
||||
type: 'resource',
|
||||
subkind: 'remote',
|
||||
sender: 'user',
|
||||
content: selected,
|
||||
threadId,
|
||||
}, {unsent: true});
|
||||
}
|
||||
|
||||
function editMessage (message) {
|
||||
let elem = inputFieldElem.value;
|
||||
if (message) {
|
||||
@@ -475,6 +512,9 @@
|
||||
<button @click="startNewThread()">
|
||||
New Thread
|
||||
</button>
|
||||
<button @click="addRemoteResource()">
|
||||
Add Remote
|
||||
</button>
|
||||
<button @click="noteMode=!noteMode">
|
||||
{{noteMode ? 'Cancel Note' : 'Make Note'}}
|
||||
</button>
|
||||
|
||||
@@ -5,72 +5,7 @@ import * as backend from '../backend';
|
||||
import * as ui from '../ui';
|
||||
|
||||
/** Connected external tool remotes (initialized with mock data) */
|
||||
const remotes = ref([/*
|
||||
{
|
||||
name: 'weather-service',
|
||||
connected: true,
|
||||
functions: [
|
||||
{
|
||||
name: 'get_forecast',
|
||||
description: 'Retrieve weather forecast for a location.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
city: { type: 'string', description: 'City name or code.' },
|
||||
days: { type: 'number', description: 'Number of days ahead.' }
|
||||
},
|
||||
required: ['city']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'search_storms',
|
||||
description: 'Find active storm systems near coordinates.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
lat: { type: 'number', description: 'Latitude.' },
|
||||
lon: { type: 'number', description: 'Longitude.' },
|
||||
radius_km: { type: 'number', description: 'Search radius.' }
|
||||
},
|
||||
required: ['lat', 'lon']
|
||||
}
|
||||
}
|
||||
],
|
||||
agentIds: []
|
||||
},
|
||||
{
|
||||
name: 'code-repo-indexer',
|
||||
connected: true,
|
||||
functions: [
|
||||
{
|
||||
name: 'search_codebase',
|
||||
description: 'Search repository code by query.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Search text.' },
|
||||
lang: { type: 'string', description: 'Language filter.' },
|
||||
path_regex: { type: 'string', description: 'Path pattern.' }
|
||||
},
|
||||
required: ['query']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'find_symbol',
|
||||
description: 'Locate a symbol definition in the codebase.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
symbol_name: { type: 'string', description: 'Name of the symbol.' },
|
||||
file_path: { type: 'string', description: 'Restrict to a file.' }
|
||||
},
|
||||
required: ['symbol_name']
|
||||
}
|
||||
}
|
||||
],
|
||||
agentIds: []
|
||||
}
|
||||
*/]);
|
||||
const remotes = ref([]);
|
||||
|
||||
/** Tracks expanded remote panels using a Set to support multiple open sections */
|
||||
const expandedRemoteIds = ref(new Set());
|
||||
@@ -113,7 +48,7 @@ onMounted(async () => {
|
||||
remotes.value = remotes.value.filter((r) => r.name != ev.data.remotename);
|
||||
});
|
||||
|
||||
if (!callListenerCancel) {
|
||||
if (callListenerCancel == null) {
|
||||
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);
|
||||
@@ -448,7 +383,7 @@ function toggleFnExpanded(index, fIdx) {
|
||||
<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>
|
||||
<input type="text" v-model="fn.name" class="input-field"/>
|
||||
<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' }}
|
||||
|
||||
Reference in new issue
Block a user