Skip to content

Commit bc790aa

Browse files
committed
feat(relay): add doctor CLI + copy env; ui: scroll no-op indicator
1 parent 66e9432 commit bc790aa

File tree

7 files changed

+177
-1
lines changed

7 files changed

+177
-1
lines changed

SKILL.MD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ List agents:
6363
npm run relay -- agents
6464
```
6565

66+
Doctor (sanity-check daemon, auth, agent connection, tool forwarding):
67+
```bash
68+
npm run relay -- doctor
69+
```
70+
6671
Get/set default agent:
6772
```bash
6873
npm run relay -- default-agent get

packages/extension/sidepanel/styles/tools.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,3 +373,20 @@
373373
padding-left: 6px;
374374
border-left: 1px solid var(--ink-1);
375375
}
376+
377+
.tool-note {
378+
font-family: var(--font-mono);
379+
font-size: 10px;
380+
letter-spacing: 0.06em;
381+
text-transform: uppercase;
382+
color: var(--muted-dim);
383+
padding: 1px 6px;
384+
border-radius: 999px;
385+
border: 1px solid var(--ink-1);
386+
background: hsla(30, 5%, 100%, 0.02);
387+
flex-shrink: 0;
388+
}
389+
390+
.tool-row.noop {
391+
opacity: 0.85;
392+
}

packages/extension/sidepanel/templates/panels/settings-general.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ <h3 class="setup-title">Active Profile</h3>
8585
<button id="saveRelayBtn" class="btn btn-primary" style="width: 100%; margin-top: 4px;">
8686
Apply Relay Settings
8787
</button>
88+
<button id="copyRelayEnvBtn" class="btn btn-secondary" style="width: 100%; margin-top: 6px;">
89+
Copy Relay Env Vars
90+
</button>
8891
</div>
8992
</details>
9093

packages/extension/sidepanel/ui/chat/panel-tools.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,13 @@ const toolIcons: Record<string, string> = {
169169
entry.endTime = Date.now();
170170
const isError = result && (result.error || result.success === false);
171171
const duration = entry.endTime - entry.startTime;
172+
const isNoopScroll =
173+
entry.fullToolName === 'scroll' && result && result.success === true && result.moved === false;
172174

173175
// Update visual state - subtle, no red/green
174176
entry.element.classList.remove('running');
175177
entry.element.classList.add(isError ? 'error' : 'done');
178+
entry.element.classList.toggle('noop', isNoopScroll);
176179

177180
// Update duration display
178181
if (entry.durationEl) {
@@ -183,6 +186,29 @@ const toolIcons: Record<string, string> = {
183186
entry.statusEl.textContent = isError ? 'ERR' : 'OK';
184187
}
185188

189+
// When scroll can't move (common in nested scroll containers), surface it without marking as an error.
190+
if (isNoopScroll) {
191+
entry.element.title = 'Scroll did not move. The page may use an inner scroll container; pass scroll.selector.';
192+
let noteEl = entry.element.querySelector('.tool-note') as HTMLElement | null;
193+
if (!noteEl) {
194+
noteEl = document.createElement('span');
195+
noteEl.className = 'tool-note';
196+
noteEl.textContent = 'no-op';
197+
// Prefer placing after args, before status.
198+
const argsEl = entry.element.querySelector('.tool-args');
199+
if (argsEl && argsEl.parentElement) {
200+
argsEl.insertAdjacentElement('afterend', noteEl);
201+
} else {
202+
const statusEl = entry.element.querySelector('.tool-status');
203+
if (statusEl && statusEl.parentElement) {
204+
statusEl.insertAdjacentElement('beforebegin', noteEl);
205+
} else {
206+
entry.element.appendChild(noteEl);
207+
}
208+
}
209+
}
210+
}
211+
186212
// Store result for potential expansion
187213
entry.result = result;
188214
};

packages/extension/sidepanel/ui/core/panel-core.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,43 @@ import { SidePanelUI } from './panel-ui.js';
105105
} catch {}
106106
});
107107

108+
this.elements.copyRelayEnvBtn?.addEventListener('click', async () => {
109+
const rawUrl = String(this.elements.relayUrl?.value || '').trim();
110+
const token = String(this.elements.relayToken?.value || '').trim();
111+
if (!rawUrl) {
112+
this.updateStatus('Enter a relay URL first', 'warning');
113+
return;
114+
}
115+
if (!token) {
116+
this.updateStatus('Enter a relay token first', 'warning');
117+
return;
118+
}
119+
120+
let host = '127.0.0.1';
121+
let port = '17373';
122+
try {
123+
const url = new URL(rawUrl);
124+
host = url.hostname || host;
125+
port = url.port || port;
126+
} catch {
127+
const cleaned = rawUrl.replace(/^https?:\/\//, '');
128+
const [h, p] = cleaned.split(':');
129+
if (h) host = h;
130+
if (p) port = p;
131+
}
132+
133+
const text = `export PARCHI_RELAY_TOKEN="${token}"
134+
export PARCHI_RELAY_HOST="${host}"
135+
export PARCHI_RELAY_PORT="${port}"`;
136+
137+
try {
138+
await navigator.clipboard.writeText(text);
139+
this.updateStatus('Relay env vars copied', 'success');
140+
} catch {
141+
this.updateStatus('Unable to copy relay env vars', 'error');
142+
}
143+
});
144+
108145
// Cancel settings
109146
this.elements.cancelSettingsBtn?.addEventListener('click', () => {
110147
void this.cancelSettings();

packages/extension/sidepanel/ui/core/panel-elements.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export const getSidePanelElements = (): SidePanelElements => ({
9292
relayUrl: byId<HTMLInputElement>('relayUrl'),
9393
relayToken: byId<HTMLInputElement>('relayToken'),
9494
saveRelayBtn: byId<HTMLButtonElement>('saveRelayBtn'),
95+
copyRelayEnvBtn: byId<HTMLButtonElement>('copyRelayEnvBtn'),
9596
relayConnectedBadge: byId<HTMLElement>('relayConnectedBadge'),
9697
relayLastErrorText: byId<HTMLElement>('relayLastErrorText'),
9798

packages/relay-service/src/cli.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ const main = async () => {
8282
print({
8383
usage: [
8484
'parchi-relay rpc <method> [--params=\'{...}\'] [--agentId=...]',
85+
'parchi-relay doctor [--agentId=...] [--skipTool=true]',
8586
'parchi-relay agents',
8687
'parchi-relay default-agent get|set <agentId>',
8788
'parchi-relay tools [--agentId=...]',
@@ -102,6 +103,93 @@ const main = async () => {
102103
return;
103104
}
104105

106+
if (cmd === 'doctor') {
107+
const agentId = flags.agentId;
108+
const skipTool = flags.skipTool === 'true';
109+
110+
const report: Record<string, any> = {
111+
ok: true,
112+
target: { host, port },
113+
checks: {},
114+
};
115+
116+
const fail = (name: string, err: unknown) => {
117+
report.ok = false;
118+
report.checks[name] = {
119+
ok: false,
120+
error: err instanceof Error ? err.message : String(err ?? 'error'),
121+
};
122+
};
123+
124+
try {
125+
const ping = await fetchRpc({ host, port, token, method: 'relay.ping' });
126+
report.checks.ping = { ok: true, result: ping };
127+
} catch (err) {
128+
fail('ping', err);
129+
print(report);
130+
process.exit(2);
131+
}
132+
133+
let agents: any[] = [];
134+
try {
135+
agents = (await fetchRpc({ host, port, token, method: 'agents.list' })) as any[];
136+
report.checks.agents = { ok: true, count: Array.isArray(agents) ? agents.length : 0, agents };
137+
} catch (err) {
138+
fail('agents', err);
139+
}
140+
141+
let resolvedAgentId: string | null = agentId || null;
142+
if (!resolvedAgentId) {
143+
try {
144+
const def = (await fetchRpc({ host, port, token, method: 'agents.default.get' })) as any;
145+
resolvedAgentId = typeof def?.agentId === 'string' ? def.agentId : null;
146+
report.checks.defaultAgent = { ok: true, agentId: resolvedAgentId };
147+
} catch (err) {
148+
fail('defaultAgent', err);
149+
}
150+
} else {
151+
report.checks.defaultAgent = { ok: true, agentId: resolvedAgentId, source: 'flag' };
152+
}
153+
154+
const connected = Array.isArray(agents) ? agents.some((a) => a?.agentId === resolvedAgentId) : false;
155+
if (resolvedAgentId && !connected) {
156+
report.ok = false;
157+
report.checks.agentConnected = {
158+
ok: false,
159+
agentId: resolvedAgentId,
160+
hint: 'AgentId not in agents.list. Ensure the extension is loaded from dist/ and Relay is enabled/applied.',
161+
};
162+
} else {
163+
report.checks.agentConnected = { ok: true, agentId: resolvedAgentId };
164+
}
165+
166+
try {
167+
const params = resolvedAgentId ? { agentId: resolvedAgentId } : undefined;
168+
const tools = await fetchRpc({ host, port, token, method: 'tools.list', params });
169+
report.checks.tools = { ok: true, toolCount: Array.isArray(tools) ? tools.length : null, tools };
170+
} catch (err) {
171+
fail('tools', err);
172+
}
173+
174+
if (!skipTool) {
175+
try {
176+
const params = resolvedAgentId
177+
? { agentId: resolvedAgentId, tool: 'getTabs', args: {} }
178+
: { tool: 'getTabs', args: {} };
179+
const result = await fetchRpc({ host, port, token, method: 'tool.call', params });
180+
report.checks.forwarding = { ok: true, tool: 'getTabs', result };
181+
} catch (err) {
182+
fail('forwarding', err);
183+
}
184+
} else {
185+
report.checks.forwarding = { ok: true, skipped: true };
186+
}
187+
188+
print(report);
189+
if (!report.ok) process.exit(2);
190+
return;
191+
}
192+
105193
if (cmd === 'agents') {
106194
const result = await fetchRpc({ host, port, token, method: 'agents.list' });
107195
print(result);
@@ -176,4 +264,3 @@ const main = async () => {
176264
};
177265

178266
await main();
179-

0 commit comments

Comments
 (0)