tool card UI redesign: semantic icons, inline diff persistence, tool detail page, MCP-friendly titles
Nightly Build / build (push) Successful in 6m38s
Nightly Build / build (push) Successful in 6m38s
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { LitElement, html, nothing } from 'lit';
|
||||
import { t } from '../../lib/i18n.js';
|
||||
import { fetchToolDetail, renderToolBody, STATUS_ICON } from './tool-detail-view.js';
|
||||
|
||||
/**
|
||||
* Mobile tool-execution detail page. Same shared engine as the desktop
|
||||
* `<tool-detail-page>`, but prop-driven: `<mobile-app>` binds `visible` / `toolId`
|
||||
* from its hash router (`#tool_detail?id=...`) instead of the component listening to
|
||||
* the hash. The back button returns to the previous mobile section via history.
|
||||
*/
|
||||
export class MobileToolDetailPage extends LitElement {
|
||||
// No shadow DOM — inherit the app's global CSS + Bootstrap Icons.
|
||||
createRenderRoot() { return this; }
|
||||
|
||||
static properties = {
|
||||
visible: { type: Boolean },
|
||||
toolId: { attribute: 'tool-id' },
|
||||
_loading: { state: true },
|
||||
_error: { state: true },
|
||||
_tool: { state: true },
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.visible = false;
|
||||
this.toolId = null;
|
||||
this._loading = false;
|
||||
this._error = null;
|
||||
this._tool = null;
|
||||
}
|
||||
|
||||
updated(changed) {
|
||||
if (changed.has('visible') || changed.has('toolId')) {
|
||||
if (this.visible && this.toolId != null) this._load();
|
||||
}
|
||||
}
|
||||
|
||||
async _load() {
|
||||
this._loading = true;
|
||||
this._error = null;
|
||||
this._tool = null;
|
||||
try {
|
||||
this._tool = await fetchToolDetail(this.toolId);
|
||||
} catch (e) {
|
||||
this._error = e.message || String(e);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
_back() { history.back(); }
|
||||
|
||||
render() {
|
||||
if (!this.visible) return nothing;
|
||||
const tl = this._tool;
|
||||
const si = tl ? (STATUS_ICON[tl.status] || STATUS_ICON.done) : null;
|
||||
return html`
|
||||
<div class="mobile-tool-detail tool-detail-page">
|
||||
<div class="mobile-section-header">
|
||||
<span class="mobile-section-title">
|
||||
<button class="chat-page-back" title=${t('fv.back')} @click=${() => this._back()}>
|
||||
<i class="bi bi-arrow-left"></i>
|
||||
</button>
|
||||
<span>
|
||||
${si ? html`<i class="bi ${si.glyph} ${si.cls} me-1"></i>` : nothing}
|
||||
${tl ? (tl.display_name || tl.name) : t('tool_detail.title')}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="tool-detail-body">
|
||||
${this._loading ? html`<div class="tool-detail-muted">${t('common.loading')}</div>` : nothing}
|
||||
${this._error ? html`<div class="alert alert-danger">${this._error}</div>` : nothing}
|
||||
${renderToolBody(tl)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('mobile-tool-detail-page', MobileToolDetailPage);
|
||||
@@ -0,0 +1,77 @@
|
||||
import { html, nothing } from 'lit';
|
||||
import { t } from '../../lib/i18n.js';
|
||||
import { openFile } from '../../lib/open-file.js';
|
||||
import { renderDiff } from '../copilot-render.js';
|
||||
|
||||
// Shared engine for the tool-execution detail view — the fetch + the center-panel
|
||||
// body — so the desktop (`tool-detail-page.js`) and mobile
|
||||
// (`shared/tool-detail-mobile.js`) surfaces render identically and only supply
|
||||
// their own chrome (header / back button).
|
||||
|
||||
export const STATUS_ICON = {
|
||||
done: { glyph: 'bi-check-circle-fill', cls: 'text-success' },
|
||||
error: { glyph: 'bi-x-circle-fill', cls: 'text-danger' },
|
||||
cancelled: { glyph: 'bi-slash-circle-fill', cls: 'text-secondary' },
|
||||
rejected: { glyph: 'bi-shield-fill-x', cls: 'text-warning' },
|
||||
pending: { glyph: 'bi-hourglass-split', cls: 'text-warning' },
|
||||
};
|
||||
|
||||
function prettyJson(v) {
|
||||
if (v == null) return '';
|
||||
try { return JSON.stringify(v, null, 2); }
|
||||
catch { return String(v); }
|
||||
}
|
||||
|
||||
/** Fetches one tool call's full detail from `GET /api/tools/{id}`. Throws on error. */
|
||||
export async function fetchToolDetail(id) {
|
||||
const r = await fetch(`/api/tools/${encodeURIComponent(id)}`, { credentials: 'same-origin' });
|
||||
if (!r.ok) throw new Error((await r.text()) || `HTTP ${r.status}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
function renderResult(tl) {
|
||||
if (tl.status === 'error' || tl.status === 'cancelled' || tl.status === 'rejected') {
|
||||
return html`<pre class="tool-detail-pre tool-detail-pre--error">${tl.error ?? tl.result ?? ''}</pre>`;
|
||||
}
|
||||
if (tl.status === 'pending') {
|
||||
return html`<div class="tool-detail-muted">${t('approval.pending')}</div>`;
|
||||
}
|
||||
let body = tl.result ?? '';
|
||||
if (tl.result_type === 'json') {
|
||||
try { body = prettyJson(JSON.parse(tl.result ?? 'null')); } catch { /* keep raw */ }
|
||||
}
|
||||
return html`<pre class="tool-detail-pre">${body}</pre>`;
|
||||
}
|
||||
|
||||
/** The center-panel body: target path, input args, diff (writes), and result. */
|
||||
export function renderToolBody(tl) {
|
||||
if (!tl) return nothing;
|
||||
const hasPreview = tl.preview_new != null || tl.preview_old != null;
|
||||
return html`
|
||||
${tl.path ? html`
|
||||
<div class="tool-detail-section">
|
||||
<span class="tool-detail-label">${t('tool_detail.target')}</span>
|
||||
<button class="tool-detail-path" @click=${() => openFile(tl.path)}>
|
||||
<i class="bi bi-file-earmark-text me-1"></i>${tl.path}
|
||||
</button>
|
||||
</div>
|
||||
` : nothing}
|
||||
|
||||
<div class="tool-detail-section">
|
||||
<span class="tool-detail-label">${t('tool_detail.input')}</span>
|
||||
<pre class="tool-detail-pre">${prettyJson(tl.arguments)}</pre>
|
||||
</div>
|
||||
|
||||
${hasPreview ? html`
|
||||
<div class="tool-detail-section">
|
||||
<span class="tool-detail-label">${t('copilot.changes')}</span>
|
||||
<pre class="copilot-diff">${renderDiff(tl.preview_old || '', tl.preview_new || '')}</pre>
|
||||
</div>
|
||||
` : nothing}
|
||||
|
||||
<div class="tool-detail-section">
|
||||
<span class="tool-detail-label">${t('copilot.result')}</span>
|
||||
${renderResult(tl)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user