diff --git a/vscode-appdaemon/README.md b/vscode-appdaemon/README.md index 9a2f3e3..5c26a6b 100644 --- a/vscode-appdaemon/README.md +++ b/vscode-appdaemon/README.md @@ -17,7 +17,7 @@ By default, it reads the existing AppDaemon text logs from the workspace: - `logs/error.log` - `logs/virtualsensors.log` -The explorer groups lines by the app name in the standard AppDaemon log format. It provides INFO/WARNING/ERROR counts, expandable app histories, relative timestamps, sorting by latest activity or app name, text/time/level filters, live updates, and raw-log navigation. Duplicate error lines found in multiple files are merged. The log reader handles file rotation, truncation, multiline exceptions, UTF-8, and legacy Windows-1252 bytes. +The explorer groups lines by the app name in the standard AppDaemon log format. It provides INFO/WARNING/ERROR counts, expandable app histories, links to app definitions, relative timestamps, sorting by latest activity or app name, text/time/level filters, live updates, and raw-log navigation. Duplicate error lines found in multiple files are merged. The log reader handles file rotation, truncation, multiline exceptions, UTF-8, and legacy Windows-1252 bytes. **AD Logs** turns red when the loaded logs contain an ERROR or CRITICAL entry. **Clear logs** empties the configured log files and clears the current explorer history. diff --git a/vscode-appdaemon/media/logExplorer.css b/vscode-appdaemon/media/logExplorer.css index cc804bc..8f58d02 100644 --- a/vscode-appdaemon/media/logExplorer.css +++ b/vscode-appdaemon/media/logExplorer.css @@ -13,3 +13,4 @@ summary{display:grid;grid-template-columns:auto minmax(0,1fr) repeat(3,max-content);align-items:center;column-gap:10px;row-gap:5px} summary .last-log{grid-column:1/-1;min-width:0} .rows{max-height:min(45vh,420px);overflow-y:auto;scrollbar-gutter:stable;overflow-anchor:none} +.app-label{flex-direction:row;align-items:center;gap:6px}.app-label .name{min-width:0}.app-link{color:var(--accent);font-size:13px;line-height:1;text-decoration:none;white-space:nowrap}.app-link:hover{text-decoration:underline}.app-link:focus-visible{outline:2px solid var(--accent);outline-offset:2px} diff --git a/vscode-appdaemon/media/logExplorer.js b/vscode-appdaemon/media/logExplorer.js index 34158ee..1e4f27c 100644 --- a/vscode-appdaemon/media/logExplorer.js +++ b/vscode-appdaemon/media/logExplorer.js @@ -161,6 +161,15 @@ const last = lastLogs.get(app); let preview; label.append(node('span', app, 'name')); + const appLink = node('a', '↗', 'app-link'); + appLink.href = '#'; + appLink.title = `Open ${app} definition`; + appLink.setAttribute('aria-label', `Open ${app} definition`); + appLink.onclick = event => { + event.preventDefault(); event.stopPropagation(); + vscode.postMessage({ type: 'app', app }); + }; + label.append(appLink); if (last) { const line = node('span', undefined, `last-log log-message level-${normalLevel(last)}`); const age = node('span', shortTimeSince(last.time), 'last-age'); diff --git a/vscode-appdaemon/src/extension.ts b/vscode-appdaemon/src/extension.ts index 9a71d8a..d871e5a 100644 --- a/vscode-appdaemon/src/extension.ts +++ b/vscode-appdaemon/src/extension.ts @@ -444,10 +444,12 @@ export async function activate(context: vscode.ExtensionContext) { editor.revealRange(new vscode.Range(pos, pos), vscode.TextEditorRevealType.InCenter); }), - vscode.commands.registerCommand('appdaemon.goToApp', async () => { - const apps = await findAllApps(); + vscode.commands.registerCommand('appdaemon.goToApp', async (requestedName?: string) => { + const apps = (await findAllApps()).filter(app => !requestedName || app.appName === requestedName); if (apps.length === 0) { - vscode.window.showWarningMessage('AppDaemon: No app definitions found in workspace'); + vscode.window.showWarningMessage(requestedName + ? `AppDaemon: No definition found for ${requestedName}` + : 'AppDaemon: No app definitions found in workspace'); return; } @@ -459,7 +461,7 @@ export async function activate(context: vscode.ExtensionContext) { app })); - const picked = await vscode.window.showQuickPick(items, { + const picked = apps.length === 1 && requestedName ? items[0] : await vscode.window.showQuickPick(items, { placeHolder: 'Go to AppDaemon app…', matchOnDescription: true, matchOnDetail: false diff --git a/vscode-appdaemon/src/logExplorer.ts b/vscode-appdaemon/src/logExplorer.ts index a2fb5a1..6b2e836 100644 --- a/vscode-appdaemon/src/logExplorer.ts +++ b/vscode-appdaemon/src/logExplorer.ts @@ -58,6 +58,9 @@ export class LogExplorer implements vscode.Disposable { this.messageListener?.dispose(); this.messageListener = webview.onDidReceiveMessage(async message => { if (message.type === 'ready') { this.publish(); } + if (message.type === 'app' && typeof message.app === 'string') { + await vscode.commands.executeCommand('appdaemon.goToApp', message.app); + } if (message.type === 'open' && typeof message.id === 'string') { const entry = this.entries.get(message.id); if (!entry?.file) { return; } diff --git a/vscode-appdaemon/tests/logUi.test.js b/vscode-appdaemon/tests/logUi.test.js index 51b6fc2..228d159 100644 --- a/vscode-appdaemon/tests/logUi.test.js +++ b/vscode-appdaemon/tests/logUi.test.js @@ -51,6 +51,17 @@ test('selects individual logs and entire initialization groups',()=>{ const input=d.getElementById('search'),paste=new dom.window.KeyboardEvent('keydown',{key:'v',ctrlKey:true,bubbles:true,cancelable:true});input.dispatchEvent(paste);assert.equal(paste.defaultPrevented,false); assert.deepEqual(errors,[]);dom.window.close(); }); + +test('app link opens its YAML definition without expanding the log group',async()=>{ + const {dom,d,sent}=setup(); + const app=d.querySelector('[data-app="alpha"]'),link=app.querySelector('.app-link'); + assert.equal(link.getAttribute('aria-label'),'Open alpha definition'); + link.click(); + await new Promise(resolve=>setTimeout(resolve,0)); + assert.equal(sent.at(-1).type,'app');assert.equal(sent.at(-1).app,'alpha'); + assert.equal(app.open,false); + dom.window.close(); +}); test('retains selected text during live updates and leaves Ctrl+C to the browser',async()=>{ const {dom,d,entries,errors,send}=setup();const alpha=d.querySelector('[data-app="alpha"]');alpha.open=true;alpha.dispatchEvent(new dom.window.Event('toggle')); const text=alpha.querySelector('.row-message'),range=d.createRange();range.selectNodeContents(text);dom.window.getSelection().addRange(range); diff --git a/vscode-appdaemon/vscode-appdaemon-0.2.0.vsix b/vscode-appdaemon/vscode-appdaemon-0.2.0.vsix index 2323fa8..36c94e5 100644 Binary files a/vscode-appdaemon/vscode-appdaemon-0.2.0.vsix and b/vscode-appdaemon/vscode-appdaemon-0.2.0.vsix differ