From 0afe018452143f49eba3e6253db1d21a001948c8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?L=27=C3=A9lectron=20rare?=
<108685187+electron-rare@users.noreply.github.com>
Date: Sat, 21 Mar 2026 09:07:11 +0100
Subject: [PATCH] feat: initialize VS Code Studio extension
---
.gitignore | 5 +
.vscode/launch.json | 18 +++
.vscode/tasks.json | 22 ++++
.vscodeignore | 5 +
LICENSE | 21 ++++
README.md | 73 +++++++++++
media/kill-life.svg | 7 ++
package-lock.json | 287 ++++++++++++++++++++++++++++++++++++++++++++
package.json | 184 ++++++++++++++++++++++++++++
src/agentCatalog.ts | 232 +++++++++++++++++++++++++++++++++++
src/config.ts | 83 +++++++++++++
src/extension.ts | 265 ++++++++++++++++++++++++++++++++++++++++
src/tree.ts | 99 +++++++++++++++
tsconfig.json | 24 ++++
14 files changed, 1325 insertions(+)
create mode 100644 .gitignore
create mode 100644 .vscode/launch.json
create mode 100644 .vscode/tasks.json
create mode 100644 .vscodeignore
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 media/kill-life.svg
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 src/agentCatalog.ts
create mode 100644 src/config.ts
create mode 100644 src/extension.ts
create mode 100644 src/tree.ts
create mode 100644 tsconfig.json
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..068db39
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+out/
+.DS_Store
+.vscode-test/
+*.vsix
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..c5083e1
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,18 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Run Kill LIFE Studio",
+ "type": "extensionHost",
+ "request": "launch",
+ "runtimeExecutable": "${execPath}",
+ "args": [
+ "--extensionDevelopmentPath=${workspaceFolder}"
+ ],
+ "outFiles": [
+ "${workspaceFolder}/out/**/*.js"
+ ],
+ "preLaunchTask": "npm: compile"
+ }
+ ]
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..f8aeba6
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,22 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "npm",
+ "script": "compile",
+ "problemMatcher": "$tsc",
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ },
+ "label": "npm: compile"
+ },
+ {
+ "type": "npm",
+ "script": "watch",
+ "problemMatcher": "$tsc-watch",
+ "isBackground": true,
+ "label": "npm: watch"
+ }
+ ]
+}
diff --git a/.vscodeignore b/.vscodeignore
new file mode 100644
index 0000000..37ace7f
--- /dev/null
+++ b/.vscodeignore
@@ -0,0 +1,5 @@
+node_modules/**
+.vscode/**
+src/**
+tsconfig.json
+README.md
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3307c84
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 electron-rare
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..17d60df
--- /dev/null
+++ b/README.md
@@ -0,0 +1,73 @@
+# Kill LIFE Studio for VS Code
+
+Extension VS Code orientee produit pour relier plusieurs projets locaux, reutiliser leurs prompts agents et transformer des idees en specs, cadrage UX et roadmap.
+
+## Positionnement
+
+`Kill LIFE Studio` est la version produit du trio :
+
+- `Studio` pour le cadrage produit, les specs, la narration, le scope et la clarte des livrables.
+- `Mesh` pour l'orchestration multi-repo et les handoffs.
+- `Operator` pour l'execution et l'operationnel.
+
+## Ce que fait cette extension
+
+- Scanne chaque projet ouvert dans le workspace, plus les racines ajoutees manuellement.
+- Detecte par defaut `agents/*.md` et `.github/agents/*.md`.
+- Affiche les agents trouves dans une sidebar dediee.
+- Permet d'ouvrir un prompt agent, de le copier, ou de l'inserer dans l'editeur courant.
+- Expose un participant de chat `@killstudio` avec des commandes `/agents`, `/pm`, `/architect`, `/firmware`, `/hw`, `/qa`, `/doc`.
+
+Par defaut, `@killstudio` repond avec un biais produit : besoins, priorisation, experience, spec-first et plan d'execution.
+
+## Demarrage rapide
+
+```bash
+npm install
+npm run compile
+```
+
+Ensuite dans VS Code :
+
+1. Ouvre ce repo.
+2. Lance `F5` pour ouvrir une fenetre d'extension development host.
+3. Dans la nouvelle fenetre, ouvre ton vrai workspace projet.
+4. Si besoin, ajoute d'autres depots via la commande `Kill LIFE Studio: Add Linked Project Root`.
+
+## Utilisation
+
+### Sidebar
+
+La vue `Kill LIFE Studio` liste les projets detectes puis les agents trouves.
+
+- Clic sur un agent : ouvre son fichier Markdown.
+- Menu contextuel : copie ou insere le prompt.
+
+### Chat
+
+Dans le chat Copilot / agent de VS Code :
+
+- `@killstudio /agents` : liste les projets et agents lies.
+- `@killstudio /pm` : route la demande avec le prompt PM.
+- `@killstudio /architect` : route la demande avec le prompt architecture.
+- `@killstudio /doc` : route la demande avec le prompt documentation.
+- `@killstudio` sans commande : agit comme studio produit relie aux agents du projet actif.
+
+## Reglages
+
+### `killLifeStudio.linkedProjectRoots`
+
+Liste de chemins absolus supplementaires a scanner, en plus des dossiers deja ouverts dans le workspace.
+
+### `killLifeStudio.agentSearchPatterns`
+
+Globs utilises pour decouvrir les prompts d'agents dans chaque racine projet.
+
+Valeurs par defaut :
+
+```json
+[
+ "agents/*.md",
+ ".github/agents/*.md"
+]
+```
diff --git a/media/kill-life.svg b/media/kill-life.svg
new file mode 100644
index 0000000..6813a35
--- /dev/null
+++ b/media/kill-life.svg
@@ -0,0 +1,7 @@
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..d9aca3e
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,287 @@
+{
+ "name": "kill-life-studio",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "kill-life-studio",
+ "version": "0.1.0",
+ "license": "MIT",
+ "dependencies": {
+ "fast-glob": "^3.3.3"
+ },
+ "devDependencies": {
+ "@types/node": "^25.5.0",
+ "@types/vscode": "^1.110.0",
+ "typescript": "^5.9.3"
+ },
+ "engines": {
+ "vscode": "^1.110.0"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "25.5.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
+ "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.18.0"
+ }
+ },
+ "node_modules/@types/vscode": {
+ "version": "1.110.0",
+ "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz",
+ "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.20.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
+ "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "dev": true,
+ "license": "MIT"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..f335cb4
--- /dev/null
+++ b/package.json
@@ -0,0 +1,184 @@
+{
+ "name": "kill-life-studio",
+ "displayName": "Kill LIFE Studio",
+ "description": "Product-focused VS Code extension that turns linked local agent prompts into a studio for specs, UX, and roadmap work.",
+ "version": "0.1.0",
+ "publisher": "electron-rare",
+ "license": "MIT",
+ "engines": {
+ "vscode": "^1.110.0"
+ },
+ "categories": [
+ "AI",
+ "Other"
+ ],
+ "keywords": [
+ "agents",
+ "copilot",
+ "chat",
+ "embedded",
+ "kill-life"
+ ],
+ "activationEvents": [
+ "onChatParticipant:kill-life-studio.orchestrator"
+ ],
+ "main": "./out/extension.js",
+ "contributes": {
+ "commands": [
+ {
+ "command": "killLifeStudio.refreshAgents",
+ "title": "Kill LIFE Studio: Refresh Agents"
+ },
+ {
+ "command": "killLifeStudio.addLinkedProjectRoot",
+ "title": "Kill LIFE Studio: Add Linked Project Root"
+ },
+ {
+ "command": "killLifeStudio.removeLinkedProjectRoot",
+ "title": "Kill LIFE Studio: Remove Linked Project Root"
+ },
+ {
+ "command": "killLifeStudio.openAgentFile",
+ "title": "Kill LIFE Studio: Open Agent File"
+ },
+ {
+ "command": "killLifeStudio.copyAgentPrompt",
+ "title": "Kill LIFE Studio: Copy Agent Prompt"
+ },
+ {
+ "command": "killLifeStudio.insertAgentPrompt",
+ "title": "Kill LIFE Studio: Insert Agent Prompt"
+ }
+ ],
+ "viewsContainers": {
+ "activitybar": [
+ {
+ "id": "killLifeStudioSidebar",
+ "title": "Kill LIFE Studio",
+ "icon": "media/kill-life.svg"
+ }
+ ]
+ },
+ "views": {
+ "killLifeStudioSidebar": [
+ {
+ "id": "killLifeStudio.agentsView",
+ "name": "Linked Agents"
+ }
+ ]
+ },
+ "menus": {
+ "view/title": [
+ {
+ "command": "killLifeStudio.refreshAgents",
+ "when": "view == killLifeStudio.agentsView",
+ "group": "navigation@1"
+ },
+ {
+ "command": "killLifeStudio.addLinkedProjectRoot",
+ "when": "view == killLifeStudio.agentsView",
+ "group": "navigation@2"
+ },
+ {
+ "command": "killLifeStudio.removeLinkedProjectRoot",
+ "when": "view == killLifeStudio.agentsView",
+ "group": "navigation@3"
+ }
+ ],
+ "view/item/context": [
+ {
+ "command": "killLifeStudio.openAgentFile",
+ "when": "view == killLifeStudio.agentsView && viewItem == agentItem",
+ "group": "inline"
+ },
+ {
+ "command": "killLifeStudio.copyAgentPrompt",
+ "when": "view == killLifeStudio.agentsView && viewItem == agentItem",
+ "group": "navigation@1"
+ },
+ {
+ "command": "killLifeStudio.insertAgentPrompt",
+ "when": "view == killLifeStudio.agentsView && viewItem == agentItem",
+ "group": "navigation@2"
+ }
+ ]
+ },
+ "configuration": {
+ "title": "Kill LIFE Studio",
+ "properties": {
+ "killLifeStudio.linkedProjectRoots": {
+ "type": "array",
+ "default": [],
+ "items": {
+ "type": "string"
+ },
+ "description": "Additional absolute project roots to scan for agent markdown files."
+ },
+ "killLifeStudio.agentSearchPatterns": {
+ "type": "array",
+ "default": [
+ "agents/*.md",
+ ".github/agents/*.md"
+ ],
+ "items": {
+ "type": "string"
+ },
+ "description": "Glob patterns resolved relative to each project root."
+ }
+ }
+ },
+ "chatParticipants": [
+ {
+ "id": "kill-life-studio.orchestrator",
+ "name": "killstudio",
+ "fullName": "Kill LIFE Studio",
+ "description": "Shape product work from your linked local project agents.",
+ "isSticky": true,
+ "commands": [
+ {
+ "name": "agents",
+ "description": "List linked projects and discovered agents."
+ },
+ {
+ "name": "pm",
+ "description": "Use the project management agent prompt."
+ },
+ {
+ "name": "architect",
+ "description": "Use the architecture agent prompt."
+ },
+ {
+ "name": "firmware",
+ "description": "Use the firmware agent prompt."
+ },
+ {
+ "name": "hw",
+ "description": "Use the hardware schematic agent prompt."
+ },
+ {
+ "name": "qa",
+ "description": "Use the QA agent prompt."
+ },
+ {
+ "name": "doc",
+ "description": "Use the documentation agent prompt."
+ }
+ ]
+ }
+ ]
+ },
+ "scripts": {
+ "vscode:prepublish": "npm run compile",
+ "compile": "tsc -p ./",
+ "watch": "tsc -watch -p ./",
+ "check": "tsc --noEmit -p ./"
+ },
+ "dependencies": {
+ "fast-glob": "^3.3.3"
+ },
+ "devDependencies": {
+ "@types/node": "^25.5.0",
+ "@types/vscode": "^1.110.0",
+ "typescript": "^5.9.3"
+ }
+}
diff --git a/src/agentCatalog.ts b/src/agentCatalog.ts
new file mode 100644
index 0000000..f666df9
--- /dev/null
+++ b/src/agentCatalog.ts
@@ -0,0 +1,232 @@
+import * as fs from 'node:fs/promises';
+import * as path from 'node:path';
+import fg from 'fast-glob';
+import * as vscode from 'vscode';
+import { getAllProjectRoots, getSearchPatterns } from './config';
+
+const SEARCH_IGNORES = [
+ '**/.git/**',
+ '**/node_modules/**',
+ '**/out/**',
+ '**/dist/**',
+ '**/coverage/**',
+ '**/target/**'
+];
+
+const ROLE_LABELS: Record = {
+ architect: 'Architect Agent',
+ doc: 'Documentation Agent',
+ firmware: 'Firmware Agent',
+ hw: 'Hardware Agent',
+ pm: 'PM Agent',
+ qa: 'QA Agent'
+};
+
+export interface AgentDefinition {
+ id: string;
+ role: string;
+ title: string;
+ description: string;
+ body: string;
+ file: vscode.Uri;
+ projectName: string;
+ projectRoot: string;
+ relativePath: string;
+}
+
+export interface ProjectSummary {
+ name: string;
+ root: string;
+ agents: AgentDefinition[];
+}
+
+function inferRole(relativePath: string): string {
+ const base = path.basename(relativePath, '.md').toLowerCase().replace(/[_-]?agent$/, '');
+ if (base === 'hw_schematic' || base === 'hw-schematic' || base === 'hwschematic') {
+ return 'hw';
+ }
+
+ return base;
+}
+
+function toDisplayRole(role: string): string {
+ return ROLE_LABELS[role] ?? `${role.replace(/[_-]+/g, ' ')} agent`;
+}
+
+function extractTitle(body: string, role: string): string {
+ for (const line of body.split(/\r?\n/u)) {
+ const trimmed = line.trim();
+ if (trimmed.startsWith('# ')) {
+ return trimmed.slice(2).trim();
+ }
+ }
+
+ return toDisplayRole(role);
+}
+
+function extractDescription(body: string, role: string): string {
+ for (const line of body.split(/\r?\n/u)) {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('>') || trimmed.startsWith('-')) {
+ continue;
+ }
+
+ return trimmed;
+ }
+
+ return `Prompt source for ${toDisplayRole(role)}.`;
+}
+
+function findOwningProjectRoot(filePath: string, projectRoots: string[]): string | undefined {
+ return [...projectRoots]
+ .sort((left, right) => right.length - left.length)
+ .find(root => filePath === root || filePath.startsWith(`${root}${path.sep}`));
+}
+
+function compareAgents(left: AgentDefinition, right: AgentDefinition): number {
+ return (
+ left.projectName.localeCompare(right.projectName) ||
+ left.role.localeCompare(right.role) ||
+ left.relativePath.localeCompare(right.relativePath)
+ );
+}
+
+export function groupAgentsByProject(agents: AgentDefinition[]): ProjectSummary[] {
+ const grouped = new Map();
+
+ for (const agent of agents) {
+ const existing = grouped.get(agent.projectRoot);
+ if (existing) {
+ existing.agents.push(agent);
+ continue;
+ }
+
+ grouped.set(agent.projectRoot, {
+ name: agent.projectName,
+ root: agent.projectRoot,
+ agents: [agent]
+ });
+ }
+
+ return [...grouped.values()]
+ .map(project => ({
+ ...project,
+ agents: [...project.agents].sort(compareAgents)
+ }))
+ .sort((left, right) => left.name.localeCompare(right.name));
+}
+
+export function resolveProjectRootForActiveEditor(projectRoots: string[]): string | undefined {
+ const activeDocument = vscode.window.activeTextEditor?.document.uri;
+ if (!activeDocument || activeDocument.scheme !== 'file') {
+ return projectRoots[0];
+ }
+
+ return [...projectRoots]
+ .sort((left, right) => right.length - left.length)
+ .find(root => activeDocument.fsPath === root || activeDocument.fsPath.startsWith(`${root}${path.sep}`))
+ ?? projectRoots[0];
+}
+
+export function selectAgentForRole(
+ agents: AgentDefinition[],
+ role: string,
+ preferredProjectRoot?: string
+): AgentDefinition | undefined {
+ const candidates = agents.filter(agent => agent.role === role);
+ if (!candidates.length) {
+ return undefined;
+ }
+
+ return candidates.find(agent => agent.projectRoot === preferredProjectRoot) ?? candidates[0];
+}
+
+export class AgentCatalog {
+ private readonly onDidChangeEmitter = new vscode.EventEmitter();
+ private agents: AgentDefinition[] = [];
+ private loaded = false;
+ private lastError: string | undefined;
+
+ readonly onDidChange = this.onDidChangeEmitter.event;
+
+ getAgents(): AgentDefinition[] {
+ return [...this.agents];
+ }
+
+ getProjects(): ProjectSummary[] {
+ return groupAgentsByProject(this.agents);
+ }
+
+ getLastError(): string | undefined {
+ return this.lastError;
+ }
+
+ async ensureLoaded(): Promise {
+ if (!this.loaded) {
+ await this.refresh();
+ }
+ }
+
+ async refresh(): Promise {
+ try {
+ const projectRoots = getAllProjectRoots();
+ const patterns = getSearchPatterns();
+ const discoveredFiles = new Set();
+
+ for (const projectRoot of projectRoots) {
+ const matches = await fg(patterns, {
+ cwd: projectRoot,
+ absolute: true,
+ onlyFiles: true,
+ unique: true,
+ followSymbolicLinks: false,
+ ignore: SEARCH_IGNORES
+ });
+
+ for (const match of matches) {
+ discoveredFiles.add(path.resolve(match));
+ }
+ }
+
+ const nextAgents: AgentDefinition[] = [];
+ const orderedFiles = [...discoveredFiles].sort((left, right) => left.localeCompare(right));
+
+ for (const filePath of orderedFiles) {
+ const projectRoot = findOwningProjectRoot(filePath, projectRoots);
+ if (!projectRoot) {
+ continue;
+ }
+
+ const body = await fs.readFile(filePath, 'utf8');
+ const relativePath = path.relative(projectRoot, filePath);
+ const role = inferRole(relativePath);
+
+ nextAgents.push({
+ id: `${projectRoot}:${relativePath}`,
+ role,
+ title: extractTitle(body, role),
+ description: extractDescription(body, role),
+ body,
+ file: vscode.Uri.file(filePath),
+ projectName: path.basename(projectRoot),
+ projectRoot,
+ relativePath
+ });
+ }
+
+ this.agents = nextAgents.sort(compareAgents);
+ this.lastError = undefined;
+ this.loaded = true;
+ } catch (error) {
+ this.agents = [];
+ this.lastError = error instanceof Error ? error.message : String(error);
+ this.loaded = true;
+ } finally {
+ this.onDidChangeEmitter.fire();
+ }
+ }
+
+ dispose(): void {
+ this.onDidChangeEmitter.dispose();
+ }
+}
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..44a0934
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,83 @@
+import * as path from 'node:path';
+import * as vscode from 'vscode';
+
+const NAMESPACE = 'killLifeStudio';
+const LINKED_PROJECT_ROOTS_KEY = 'linkedProjectRoots';
+const SEARCH_PATTERNS_KEY = 'agentSearchPatterns';
+
+const DEFAULT_SEARCH_PATTERNS = ['agents/*.md', '.github/agents/*.md'];
+
+function getConfiguration(): vscode.WorkspaceConfiguration {
+ return vscode.workspace.getConfiguration(NAMESPACE);
+}
+
+function normalizeProjectRoot(input: string): string {
+ return path.resolve(input.trim());
+}
+
+function getConfigurationTarget(): vscode.ConfigurationTarget {
+ return vscode.workspace.workspaceFile || vscode.workspace.workspaceFolders?.length
+ ? vscode.ConfigurationTarget.Workspace
+ : vscode.ConfigurationTarget.Global;
+}
+
+export function getLinkedProjectRoots(): string[] {
+ const configured = getConfiguration().get(LINKED_PROJECT_ROOTS_KEY) ?? [];
+ return [...new Set(configured.map(normalizeProjectRoot).filter(Boolean))];
+}
+
+export function getSearchPatterns(): string[] {
+ const configured = getConfiguration().get(SEARCH_PATTERNS_KEY) ?? [];
+ const cleaned = configured.map(pattern => pattern.trim()).filter(Boolean);
+ return cleaned.length ? cleaned : DEFAULT_SEARCH_PATTERNS;
+}
+
+export function getAllProjectRoots(): string[] {
+ const workspaceRoots = (vscode.workspace.workspaceFolders ?? []).map(folder => folder.uri.fsPath);
+ return [...new Set([...workspaceRoots, ...getLinkedProjectRoots()].map(normalizeProjectRoot))];
+}
+
+export async function addLinkedProjectRoot(): Promise {
+ const selection = await vscode.window.showOpenDialog({
+ canSelectFiles: false,
+ canSelectFolders: true,
+ canSelectMany: false,
+ openLabel: 'Link project root'
+ });
+
+ const selected = selection?.[0];
+ if (!selected) {
+ return;
+ }
+
+ const nextRoot = normalizeProjectRoot(selected.fsPath);
+ const current = getLinkedProjectRoots();
+ if (current.includes(nextRoot)) {
+ void vscode.window.showInformationMessage(`Project root already linked: ${nextRoot}`);
+ return;
+ }
+
+ const updated = [...current, nextRoot].sort((left, right) => left.localeCompare(right));
+ await getConfiguration().update(LINKED_PROJECT_ROOTS_KEY, updated, getConfigurationTarget());
+ void vscode.window.showInformationMessage(`Linked project root added: ${nextRoot}`);
+}
+
+export async function removeLinkedProjectRoot(): Promise {
+ const current = getLinkedProjectRoots();
+ if (!current.length) {
+ void vscode.window.showInformationMessage('No linked project roots configured.');
+ return;
+ }
+
+ const picked = await vscode.window.showQuickPick(current, {
+ placeHolder: 'Select a linked project root to remove'
+ });
+
+ if (!picked) {
+ return;
+ }
+
+ const updated = current.filter(root => root !== picked);
+ await getConfiguration().update(LINKED_PROJECT_ROOTS_KEY, updated, getConfigurationTarget());
+ void vscode.window.showInformationMessage(`Linked project root removed: ${picked}`);
+}
diff --git a/src/extension.ts b/src/extension.ts
new file mode 100644
index 0000000..30251f3
--- /dev/null
+++ b/src/extension.ts
@@ -0,0 +1,265 @@
+import * as vscode from 'vscode';
+import {
+ AgentCatalog,
+ AgentDefinition,
+ resolveProjectRootForActiveEditor,
+ selectAgentForRole
+} from './agentCatalog';
+import { addLinkedProjectRoot, getAllProjectRoots, removeLinkedProjectRoot } from './config';
+import { AgentsTreeProvider } from './tree';
+
+const PARTICIPANT_ID = 'kill-life-studio.orchestrator';
+
+const ROLE_COMMANDS = new Set(['pm', 'architect', 'firmware', 'hw', 'qa', 'doc']);
+
+function buildRosterMarkdown(agents: AgentDefinition[]): string {
+ if (!agents.length) {
+ return 'No agents discovered yet. Open a workspace or add linked project roots first.';
+ }
+
+ const grouped = new Map();
+ for (const agent of agents) {
+ const bucket = grouped.get(agent.projectName) ?? [];
+ bucket.push(agent);
+ grouped.set(agent.projectName, bucket);
+ }
+
+ const lines: string[] = ['# Linked agents'];
+ for (const [projectName, projectAgents] of [...grouped.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
+ lines.push(`## ${projectName}`);
+ for (const agent of projectAgents.sort((left, right) => left.role.localeCompare(right.role))) {
+ lines.push(`- \`${agent.role}\` - ${agent.title} (\`${agent.relativePath}\`)`);
+ }
+ }
+
+ return lines.join('\n');
+}
+
+function buildGroundingPrompt(
+ agents: AgentDefinition[],
+ preferredProjectRoot: string | undefined,
+ selectedAgent: AgentDefinition | undefined
+): string {
+ const projectAgents = preferredProjectRoot
+ ? agents.filter(agent => agent.projectRoot === preferredProjectRoot)
+ : agents;
+
+ const roster = (projectAgents.length ? projectAgents : agents)
+ .map(agent => `- ${agent.role}: ${agent.title} (${agent.projectName} / ${agent.relativePath})`)
+ .join('\n');
+
+ const projectLine = preferredProjectRoot
+ ? `Active project root: ${preferredProjectRoot}`
+ : 'No active project root detected.';
+
+ const agentBlock = selectedAgent
+ ? `Selected agent prompt:\n${selectedAgent.body}`
+ : 'No single agent command was selected. Act as a product studio that coordinates PM, architecture, documentation, and implementation framing.';
+
+ return [
+ 'You are Kill LIFE Studio inside VS Code.',
+ 'You focus on product thinking, scope, UX, specs, roadmap clarity, and turning ideas into executable work.',
+ 'Ground every answer in the linked local project agents listed below.',
+ 'Do not invent files, commands, standards, or project state that were not provided.',
+ 'If information is missing, say what is missing and ask for the smallest useful next input.',
+ projectLine,
+ 'Available agents:',
+ roster || '- none',
+ agentBlock
+ ].join('\n\n');
+}
+
+function extractHistory(context: vscode.ChatContext): vscode.LanguageModelChatMessage[] {
+ const previousMessages = context.history.filter(
+ historyTurn => historyTurn instanceof vscode.ChatResponseTurn
+ );
+
+ const messages: vscode.LanguageModelChatMessage[] = [];
+ for (const previousMessage of previousMessages) {
+ let fullMessage = '';
+ previousMessage.response.forEach(part => {
+ const markdownPart = part as vscode.ChatResponseMarkdownPart;
+ fullMessage += markdownPart.value.value;
+ });
+
+ if (fullMessage.trim()) {
+ messages.push(vscode.LanguageModelChatMessage.Assistant(fullMessage));
+ }
+ }
+
+ return messages;
+}
+
+async function pickAgent(
+ catalog: AgentCatalog,
+ placeHolder: string
+): Promise {
+ await catalog.ensureLoaded();
+ const agents = catalog.getAgents();
+ if (!agents.length) {
+ void vscode.window.showWarningMessage('No agents discovered yet.');
+ return undefined;
+ }
+
+ const picked = await vscode.window.showQuickPick(
+ agents.map(agent => ({
+ label: agent.title,
+ description: `${agent.projectName} • ${agent.role}`,
+ detail: agent.relativePath,
+ agent
+ })),
+ { placeHolder }
+ );
+
+ return picked?.agent;
+}
+
+async function openAgentFile(agent: AgentDefinition): Promise {
+ await vscode.window.showTextDocument(agent.file, {
+ preview: false
+ });
+}
+
+async function copyAgentPrompt(agent: AgentDefinition): Promise {
+ await vscode.env.clipboard.writeText(agent.body);
+ void vscode.window.showInformationMessage(`Copied prompt: ${agent.title}`);
+}
+
+async function insertAgentPrompt(agent: AgentDefinition): Promise {
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) {
+ const document = await vscode.workspace.openTextDocument({
+ content: agent.body,
+ language: 'markdown'
+ });
+ await vscode.window.showTextDocument(document, { preview: false });
+ return;
+ }
+
+ await editor.edit(editBuilder => {
+ editBuilder.insert(editor.selection.active, agent.body);
+ });
+}
+
+export function activate(context: vscode.ExtensionContext): void {
+ const catalog = new AgentCatalog();
+ const treeProvider = new AgentsTreeProvider(catalog);
+
+ context.subscriptions.push(catalog, treeProvider);
+ context.subscriptions.push(
+ vscode.window.registerTreeDataProvider('killLifeStudio.agentsView', treeProvider)
+ );
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand('killLifeStudio.refreshAgents', async () => {
+ await catalog.refresh();
+ })
+ );
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand('killLifeStudio.addLinkedProjectRoot', async () => {
+ await addLinkedProjectRoot();
+ await catalog.refresh();
+ })
+ );
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand('killLifeStudio.removeLinkedProjectRoot', async () => {
+ await removeLinkedProjectRoot();
+ await catalog.refresh();
+ })
+ );
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand('killLifeStudio.openAgentFile', async (agent?: AgentDefinition) => {
+ const resolvedAgent = agent ?? (await pickAgent(catalog, 'Select an agent file to open'));
+ if (resolvedAgent) {
+ await openAgentFile(resolvedAgent);
+ }
+ })
+ );
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand('killLifeStudio.copyAgentPrompt', async (agent?: AgentDefinition) => {
+ const resolvedAgent = agent ?? (await pickAgent(catalog, 'Select an agent prompt to copy'));
+ if (resolvedAgent) {
+ await copyAgentPrompt(resolvedAgent);
+ }
+ })
+ );
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand('killLifeStudio.insertAgentPrompt', async (agent?: AgentDefinition) => {
+ const resolvedAgent = agent ?? (await pickAgent(catalog, 'Select an agent prompt to insert'));
+ if (resolvedAgent) {
+ await insertAgentPrompt(resolvedAgent);
+ }
+ })
+ );
+
+ context.subscriptions.push(
+ vscode.workspace.onDidChangeConfiguration(event => {
+ if (event.affectsConfiguration('killLifeStudio')) {
+ void catalog.refresh();
+ }
+ })
+ );
+
+ context.subscriptions.push(
+ vscode.workspace.onDidChangeWorkspaceFolders(() => {
+ void catalog.refresh();
+ })
+ );
+
+ const chatHandler: vscode.ChatRequestHandler = async (
+ request,
+ chatContext,
+ stream,
+ token
+ ) => {
+ await catalog.ensureLoaded();
+
+ const agents = catalog.getAgents();
+ if (request.command === 'agents') {
+ stream.markdown(buildRosterMarkdown(agents));
+ return;
+ }
+
+ if (!agents.length) {
+ stream.markdown(
+ 'No agents discovered yet. Open a project workspace or run `Kill LIFE Studio: Add Linked Project Root` first.'
+ );
+ return;
+ }
+
+ const projectRoots = getAllProjectRoots();
+ const preferredProjectRoot = resolveProjectRootForActiveEditor(projectRoots);
+ const selectedRole = request.command && ROLE_COMMANDS.has(request.command) ? request.command : undefined;
+ const selectedAgent = selectedRole
+ ? selectAgentForRole(agents, selectedRole, preferredProjectRoot)
+ : undefined;
+
+ const messages: vscode.LanguageModelChatMessage[] = [
+ vscode.LanguageModelChatMessage.User(
+ buildGroundingPrompt(agents, preferredProjectRoot, selectedAgent)
+ ),
+ ...extractHistory(chatContext),
+ vscode.LanguageModelChatMessage.User(
+ request.prompt.trim() || 'Provide a concise product next-step plan for the selected agent.'
+ )
+ ];
+
+ const response = await request.model.sendRequest(messages, {}, token);
+ for await (const fragment of response.text) {
+ stream.markdown(fragment);
+ }
+ };
+
+ const participant = vscode.chat.createChatParticipant(PARTICIPANT_ID, chatHandler);
+ participant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'media', 'kill-life.svg');
+ context.subscriptions.push(participant);
+
+ void catalog.refresh();
+}
+
+export function deactivate(): void {}
diff --git a/src/tree.ts b/src/tree.ts
new file mode 100644
index 0000000..596163c
--- /dev/null
+++ b/src/tree.ts
@@ -0,0 +1,99 @@
+import * as vscode from 'vscode';
+import { AgentCatalog, AgentDefinition, ProjectSummary } from './agentCatalog';
+
+class ProjectNode {
+ readonly kind = 'project';
+
+ constructor(readonly project: ProjectSummary) {}
+}
+
+class AgentNode {
+ readonly kind = 'agent';
+
+ constructor(readonly agent: AgentDefinition) {}
+}
+
+class InfoNode {
+ readonly kind = 'info';
+
+ constructor(readonly message: string) {}
+}
+
+export type AgentsTreeNode = ProjectNode | AgentNode | InfoNode;
+
+export class AgentsTreeProvider implements vscode.TreeDataProvider {
+ private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter();
+
+ readonly onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event;
+
+ constructor(private readonly catalog: AgentCatalog) {
+ this.catalog.onDidChange(() => this.refresh());
+ }
+
+ refresh(): void {
+ this.onDidChangeTreeDataEmitter.fire(undefined);
+ }
+
+ getTreeItem(element: AgentsTreeNode): vscode.TreeItem {
+ if (element.kind === 'info') {
+ const infoItem = new vscode.TreeItem(element.message, vscode.TreeItemCollapsibleState.None);
+ infoItem.contextValue = 'infoItem';
+ infoItem.iconPath = new vscode.ThemeIcon('info');
+ return infoItem;
+ }
+
+ if (element.kind === 'project') {
+ const projectItem = new vscode.TreeItem(
+ element.project.name,
+ vscode.TreeItemCollapsibleState.Expanded
+ );
+ projectItem.description = `${element.project.agents.length} agent${element.project.agents.length > 1 ? 's' : ''}`;
+ projectItem.tooltip = element.project.root;
+ projectItem.contextValue = 'projectItem';
+ projectItem.iconPath = new vscode.ThemeIcon('repo');
+ return projectItem;
+ }
+
+ const agent = element.agent;
+ const agentItem = new vscode.TreeItem(agent.title, vscode.TreeItemCollapsibleState.None);
+ agentItem.description = agent.role;
+ agentItem.tooltip = `${agent.description}\n${agent.file.fsPath}`;
+ agentItem.resourceUri = agent.file;
+ agentItem.contextValue = 'agentItem';
+ agentItem.iconPath = new vscode.ThemeIcon('hubot');
+ agentItem.command = {
+ command: 'killLifeStudio.openAgentFile',
+ title: 'Open Agent File',
+ arguments: [agent]
+ };
+ return agentItem;
+ }
+
+ getChildren(element?: AgentsTreeNode): vscode.ProviderResult {
+ if (!element) {
+ const projects = this.catalog.getProjects();
+ if (!projects.length) {
+ const error = this.catalog.getLastError();
+ if (error) {
+ return [new InfoNode(`Unable to load agents: ${error}`)];
+ }
+
+ return [
+ new InfoNode('Open a project workspace or add linked project roots to discover agents.')
+ ];
+ }
+
+ return projects.map(project => new ProjectNode(project));
+ }
+
+ if (element.kind === 'project') {
+ return element.project.agents.map(agent => new AgentNode(agent));
+ }
+
+ return [];
+ }
+
+ dispose(): void {
+ this.onDidChangeTreeDataEmitter.dispose();
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..cad090f
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "Node16",
+ "moduleResolution": "Node16",
+ "lib": [
+ "ES2022"
+ ],
+ "outDir": "out",
+ "rootDir": "src",
+ "strict": true,
+ "sourceMap": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "skipLibCheck": true,
+ "types": [
+ "node",
+ "vscode"
+ ]
+ },
+ "include": [
+ "src/**/*.ts"
+ ]
+}