From fbea05763a2a558cfd68a216b825e476af651fbb Mon Sep 17 00:00:00 2001 From: Eric Simmerman Date: Fri, 6 Mar 2026 11:43:48 -0500 Subject: [PATCH 1/8] group chat enhancements --- package-lock.json | 80 ++-- src/main/group-chat/group-chat-router.ts | 353 +++++++++++++++++- src/main/index.ts | 9 + src/main/ipc/handlers/groupChat.ts | 21 ++ src/main/preload/groupChat.ts | 9 + src/main/process-listeners/data-listener.ts | 7 + src/prompts/group-chat-moderator-system.md | 19 + src/prompts/group-chat-participant-request.md | 2 +- src/renderer/components/GroupChatList.tsx | 2 +- .../components/GroupChatParticipants.tsx | 6 +- src/renderer/components/ParticipantCard.tsx | 62 ++- .../components/Settings/SettingsModal.tsx | 50 +++ src/renderer/global.d.ts | 3 + .../hooks/groupChat/useGroupChatHandlers.ts | 15 + src/renderer/hooks/settings/useSettings.ts | 4 + src/renderer/stores/groupChatStore.ts | 31 ++ src/renderer/stores/settingsStore.ts | 12 + 17 files changed, 635 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7482623e10..52ae046858 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "maestro", - "version": "0.15.0", + "version": "0.15.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "maestro", - "version": "0.15.0", + "version": "0.15.1", "hasInstallScript": true, "license": "AGPL 3.0", "dependencies": { @@ -264,6 +264,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -667,6 +668,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -710,6 +712,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2283,6 +2286,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2304,6 +2308,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz", "integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==", "license": "Apache-2.0", + "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -2316,6 +2321,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -2331,6 +2337,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.208.0.tgz", "integrity": "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.208.0", "import-in-the-middle": "^2.0.0", @@ -2718,6 +2725,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -2734,6 +2742,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", @@ -2751,6 +2760,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=14" } @@ -3809,8 +3819,7 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4348,6 +4357,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -4359,6 +4369,7 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -4484,6 +4495,7 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -4914,6 +4926,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4995,6 +5008,7 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5998,6 +6012,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -6480,6 +6495,7 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", @@ -7205,6 +7221,7 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -7614,6 +7631,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -8111,6 +8129,7 @@ "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "24.13.3", "builder-util": "24.13.1", @@ -8206,8 +8225,7 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/dompurify": { "version": "3.3.0", @@ -8351,7 +8369,6 @@ "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "24.13.3", "archiver": "^5.3.1", @@ -8365,7 +8382,6 @@ "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", @@ -8385,7 +8401,6 @@ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", @@ -8408,7 +8423,6 @@ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -8425,7 +8439,6 @@ "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", @@ -8442,7 +8455,6 @@ "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" @@ -8457,7 +8469,6 @@ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -8473,7 +8484,6 @@ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -8486,8 +8496,7 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/electron-builder-squirrel-windows/node_modules/string_decoder": { "version": "1.1.1", @@ -8495,7 +8504,6 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -8506,7 +8514,6 @@ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 10.0.0" } @@ -8517,7 +8524,6 @@ "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", @@ -8533,7 +8539,6 @@ "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", @@ -9215,6 +9220,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -11134,6 +11140,7 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -11954,6 +11961,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -12423,16 +12431,14 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", @@ -12445,8 +12451,7 @@ "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.isequal": { "version": "4.5.0", @@ -12460,8 +12465,7 @@ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -12475,8 +12479,7 @@ "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/log-symbols": { "version": "4.1.0", @@ -12567,7 +12570,6 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -15065,6 +15067,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -15305,7 +15308,6 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -15321,7 +15323,6 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -15666,6 +15667,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -15695,6 +15697,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -15742,6 +15745,7 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", + "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -15928,7 +15932,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -17685,6 +17690,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -17995,6 +18001,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18368,6 +18375,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -18873,6 +18881,7 @@ "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.0.15", "@vitest/mocker": "4.0.15", @@ -19463,6 +19472,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -19476,6 +19486,7 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -20073,6 +20084,7 @@ "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "dev": true, "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/src/main/group-chat/group-chat-router.ts b/src/main/group-chat/group-chat-router.ts index 1ac924f6ee..52fe49b835 100644 --- a/src/main/group-chat/group-chat-router.ts +++ b/src/main/group-chat/group-chat-router.ts @@ -9,6 +9,8 @@ */ import * as os from 'os'; +import * as fs from 'fs'; +import * as path from 'path'; import { GroupChatParticipant, loadGroupChat, @@ -40,7 +42,7 @@ import { applyAgentConfigOverrides, getContextWindowValue, } from '../utils/agent-args'; -import { groupChatParticipantRequestPrompt } from '../../prompts'; +import { groupChatParticipantRequestPrompt, autorunDefaultPrompt } from '../../prompts'; import { wrapSpawnWithSsh } from '../utils/ssh-spawn-wrapper'; import type { SshRemoteSettingsStore } from '../utils/ssh-remote-resolver'; import { setGetCustomShellPathCallback, getWindowsSpawnConfig } from './group-chat-config'; @@ -72,6 +74,10 @@ export interface SessionInfo { remoteId: string | null; workingDirOverride?: string; }; + /** Auto Run folder path for this session */ + autoRunFolderPath?: string; + /** Base directory for git worktrees (configured in agent settings) */ + worktreeBasePath?: string; } /** @@ -84,6 +90,10 @@ export type GetSessionsCallback = () => SessionInfo[]; */ export type GetCustomEnvVarsCallback = (agentId: string) => Record | undefined; export type GetAgentConfigCallback = (agentId: string) => Record | undefined; +export type GetModeratorSettingsCallback = () => { + standingInstructions: string; + conductorProfile: string; +}; // Module-level callback for session lookup let getSessionsCallback: GetSessionsCallback | null = null; @@ -92,6 +102,9 @@ let getSessionsCallback: GetSessionsCallback | null = null; let getCustomEnvVarsCallback: GetCustomEnvVarsCallback | null = null; let getAgentConfigCallback: GetAgentConfigCallback | null = null; +// Module-level callback for moderator settings (standing instructions + conductor profile) +let getModeratorSettingsCallback: GetModeratorSettingsCallback | null = null; + // Module-level SSH store for remote execution support let sshStore: SshRemoteSettingsStore | null = null; @@ -174,6 +187,14 @@ export function setGetAgentConfigCallback(callback: GetAgentConfigCallback): voi getAgentConfigCallback = callback; } +/** + * Sets the callback for getting moderator settings (standing instructions + conductor profile). + * Called from index.ts during initialization. + */ +export function setGetModeratorSettingsCallback(callback: GetModeratorSettingsCallback): void { + getModeratorSettingsCallback = callback; +} + /** * Sets the SSH store for remote execution support. * Called from index.ts during initialization. @@ -240,6 +261,64 @@ export function extractAllMentions(text: string): string[] { return mentions; } +/** + * Extracts !autorun directives from moderator output. + * Matches `!autorun @AgentName` patterns. + * + * @param text - The moderator's message text + * @returns Object with autorun participant names and cleaned message text + */ +export function extractAutoRunDirectives(text: string): { + autoRunParticipants: string[]; + cleanedText: string; +} { + const autoRunParticipants: string[] = []; + const autoRunPattern = /!autorun\s+@([^\s@:,;!?()\[\]{}'"<>]+)/g; + let match; + + while ((match = autoRunPattern.exec(text)) !== null) { + const name = match[1]; + if (!autoRunParticipants.includes(name)) { + autoRunParticipants.push(name); + } + } + + // Remove !autorun lines from the message for display + const cleanedText = text + .replace(/^.*!autorun\s+@[^\s@:,;!?()\[\]{}'"<>]+.*$/gm, '') + .replace(/\n{3,}/g, '\n\n') + .trim(); + + return { autoRunParticipants, cleanedText }; +} + +/** + * Reads Auto Run documents from a folder. + * + * @param folderPath - The Auto Run folder path + * @returns Array of document info with name, path, and content + */ +export function readAutoRunDocs( + folderPath: string +): { name: string; path: string; content: string }[] { + try { + if (!fs.existsSync(folderPath)) { + return []; + } + const files = fs.readdirSync(folderPath).filter((f) => f.endsWith('.md')); + return files.map((f) => { + const fullPath = path.join(folderPath, f); + return { + name: f, + path: fullPath, + content: fs.readFileSync(fullPath, 'utf-8'), + }; + }); + } catch { + return []; + } +} + /** * Routes a user message to the moderator. * @@ -411,10 +490,19 @@ export async function routeUserMessage( // Build participant context // Use normalized names (spaces → hyphens) so moderator can @mention them properly + const sessionsList = getSessionsCallback?.() || []; const participantContext = chat.participants.length > 0 ? chat.participants - .map((p) => `- @${normalizeMentionName(p.name)} (${p.agentId} session)`) + .map((p) => { + const matchingSession = sessionsList.find( + (s) => mentionMatches(s.name, p.name) || s.name === p.name + ); + const worktreeNote = matchingSession?.worktreeBasePath + ? ` [worktree base: ${matchingSession.worktreeBasePath}]` + : ''; + return `- @${normalizeMentionName(p.name)} (${p.agentId} session)${worktreeNote}`; + }) .join('\n') : '(No agents currently in this group chat)'; @@ -444,7 +532,24 @@ export async function routeUserMessage( .map((m) => `[${m.from}]: ${m.content}`) .join('\n'); - const fullPrompt = `${getModeratorSystemPrompt()} + // Get moderator settings for prompt customization + const moderatorSettings = getModeratorSettingsCallback?.() ?? { + standingInstructions: '', + conductorProfile: '', + }; + + // Substitute {{CONDUCTOR_PROFILE}} template variable + const baseSystemPrompt = getModeratorSystemPrompt().replace( + '{{CONDUCTOR_PROFILE}}', + moderatorSettings.conductorProfile || '(No conductor profile set)' + ); + + // Build standing instructions section if configured + const standingInstructionsSection = moderatorSettings.standingInstructions + ? `\n\n## Standing Instructions\n\nThe following instructions apply to ALL group chat sessions. Follow them consistently:\n\n${moderatorSettings.standingInstructions}` + : ''; + + const fullPrompt = `${baseSystemPrompt}${standingInstructionsSection} ## Current Participants: ${participantContext}${availableSessionsContext} @@ -771,10 +876,202 @@ export async function routeModeratorResponse( // Track participants that will need to respond for synthesis round const participantsToRespond = new Set(); - // Spawn batch processes for each mentioned participant - if (processManager && agentDetector && mentions.length > 0) { + // Extract !autorun directives from the moderator message + const { autoRunParticipants } = extractAutoRunDirectives(message); + if (autoRunParticipants.length > 0) { + console.log( + `[GroupChat:Debug] Found !autorun directives for: ${autoRunParticipants.join(', ')}` + ); + } + + // Spawn autorun participants + if (processManager && agentDetector && autoRunParticipants.length > 0) { + console.log(`[GroupChat:Debug] ========== SPAWNING AUTORUN AGENTS ==========`); + const sessions = getSessionsCallback?.() || []; + + for (const autoRunName of autoRunParticipants) { + // Find participant (may have been auto-added above via @mention detection) + const participant = updatedChat.participants.find((p) => mentionMatches(autoRunName, p.name)); + if (!participant) { + console.warn( + `[GroupChat:Debug] Autorun participant ${autoRunName} not found in chat - skipping` + ); + const errorMsg: GroupChatMessage = { + timestamp: new Date().toISOString(), + from: 'system', + content: `⚠️ Could not find participant @${autoRunName} for autorun execution. Make sure the agent exists and is added to the group chat.`, + }; + groupChatEmitters.emitMessage?.(groupChatId, errorMsg); + continue; + } + + const matchingSession = sessions.find( + (s) => mentionMatches(s.name, participant.name) || s.name === participant.name + ); + const cwd = matchingSession?.cwd || os.homedir(); + const autoRunFolderPath = matchingSession?.autoRunFolderPath; + + if (!autoRunFolderPath) { + console.warn( + `[GroupChat:Debug] No autoRunFolderPath configured for ${participant.name} - skipping` + ); + const errorMsg: GroupChatMessage = { + timestamp: new Date().toISOString(), + from: 'system', + content: `⚠️ No Auto Run folder configured for @${participant.name}. Configure an Auto Run folder in the agent's settings first.`, + }; + groupChatEmitters.emitMessage?.(groupChatId, errorMsg); + continue; + } + + // Read autorun documents + const docs = readAutoRunDocs(autoRunFolderPath); + if (docs.length === 0) { + console.warn( + `[GroupChat:Debug] No autorun documents found in ${autoRunFolderPath} for ${participant.name}` + ); + const errorMsg: GroupChatMessage = { + timestamp: new Date().toISOString(), + from: 'system', + content: `⚠️ No Auto Run documents (.md files) found in ${autoRunFolderPath} for @${participant.name}.`, + }; + groupChatEmitters.emitMessage?.(groupChatId, errorMsg); + continue; + } + + // Find the first unchecked document (has unchecked tasks) + const uncheckedDoc = docs.find((d) => d.content.includes('- [ ]')); + const targetDoc = uncheckedDoc || docs[0]; + + console.log( + `[GroupChat:Debug] Autorun for ${participant.name}: ${docs.length} doc(s), target: ${targetDoc.name}` + ); + + // Build the autorun prompt using the standard template + const autoRunPrompt = autorunDefaultPrompt + .replace(/\{\{AGENT_NAME\}\}/g, participant.name) + .replace(/\{\{AGENT_PATH\}\}/g, cwd) + .replace(/\{\{AUTORUN_FOLDER\}\}/g, autoRunFolderPath) + .replace(/\{\{LOOP_NUMBER\}\}/g, '1') + .replace(/\{\{GIT_BRANCH\}\}/g, '(group chat execution)') + .replace(/\{\{DOCUMENT_PATH\}\}/g, targetDoc.path); + + // Resolve agent and spawn + const agent = await agentDetector.getAgent(participant.agentId); + if (!agent || !agent.available) { + console.error( + `[GroupChat:Debug] Agent '${participant.agentId}' not available for autorun ${participant.name}` + ); + continue; + } + + const sessionId = `group-chat-${groupChatId}-participant-${participant.name}-${Date.now()}`; + const agentConfigValues = getAgentConfigCallback?.(participant.agentId) || {}; + const baseArgs = buildAgentArgs(agent, { + baseArgs: [...agent.args], + prompt: autoRunPrompt, + cwd, + readOnlyMode: false, // Autorun always needs write access + agentSessionId: participant.agentSessionId, + }); + const configResolution = applyAgentConfigOverrides(agent, baseArgs, { + agentConfigValues, + sessionCustomModel: matchingSession?.customModel, + sessionCustomArgs: matchingSession?.customArgs, + sessionCustomEnvVars: matchingSession?.customEnvVars, + }); + + try { + groupChatEmitters.emitParticipantState?.(groupChatId, participant.name, 'working'); + + // Prepare spawn config with potential SSH wrapping + let finalSpawnCommand = agent.path || agent.command; + let finalSpawnArgs = configResolution.args; + let finalSpawnCwd = cwd; + let finalSpawnPrompt: string | undefined = autoRunPrompt; + let finalSpawnEnvVars = + configResolution.effectiveCustomEnvVars ?? + getCustomEnvVarsCallback?.(participant.agentId); + let finalSpawnShell: string | undefined; + let finalSpawnRunInShell = false; + + if (sshStore && matchingSession?.sshRemoteConfig) { + const sshWrapped = await wrapSpawnWithSsh( + { + command: finalSpawnCommand, + args: finalSpawnArgs, + cwd, + prompt: autoRunPrompt, + customEnvVars: + configResolution.effectiveCustomEnvVars ?? + getCustomEnvVarsCallback?.(participant.agentId), + promptArgs: agent.promptArgs, + noPromptSeparator: agent.noPromptSeparator, + agentBinaryName: agent.binaryName, + }, + matchingSession.sshRemoteConfig, + sshStore + ); + finalSpawnCommand = sshWrapped.command; + finalSpawnArgs = sshWrapped.args; + finalSpawnCwd = sshWrapped.cwd; + finalSpawnPrompt = sshWrapped.prompt; + finalSpawnEnvVars = sshWrapped.customEnvVars; + } + + const winConfig = getWindowsSpawnConfig( + participant.agentId, + matchingSession?.sshRemoteConfig + ); + if (winConfig.shell) { + finalSpawnShell = winConfig.shell; + finalSpawnRunInShell = winConfig.runInShell; + } + + processManager.spawn({ + sessionId, + toolType: participant.agentId, + cwd: finalSpawnCwd, + command: finalSpawnCommand, + args: finalSpawnArgs, + readOnlyMode: false, // Autorun always needs write access + prompt: finalSpawnPrompt, + contextWindow: getContextWindowValue(agent, agentConfigValues), + customEnvVars: finalSpawnEnvVars, + promptArgs: agent.promptArgs, + noPromptSeparator: agent.noPromptSeparator, + shell: finalSpawnShell, + runInShell: finalSpawnRunInShell, + sendPromptViaStdin: winConfig.sendPromptViaStdin, + sendPromptViaStdinRaw: winConfig.sendPromptViaStdinRaw, + }); + + participantsToRespond.add(participant.name); + console.log( + `[GroupChat:Debug] Spawned autorun process for @${participant.name} (session ${sessionId})` + ); + } catch (error) { + logger.error(`Failed to spawn autorun participant ${participant.name}`, LOG_CONTEXT, { + error, + groupChatId, + }); + captureException(error, { + operation: 'groupChat:spawnAutorunParticipant', + participantName: participant.name, + groupChatId, + }); + } + } + console.log(`[GroupChat:Debug] =================================================`); + } + + // Spawn batch processes for each mentioned participant (exclude autorun participants) + const mentionsToSpawn = mentions.filter( + (name) => !autoRunParticipants.some((arName) => mentionMatches(arName, name)) + ); + if (processManager && agentDetector && mentionsToSpawn.length > 0) { console.log(`[GroupChat:Debug] ========== SPAWNING PARTICIPANT AGENTS ==========`); - console.log(`[GroupChat:Debug] Will spawn ${mentions.length} participant agent(s)`); + console.log(`[GroupChat:Debug] Will spawn ${mentionsToSpawn.length} participant agent(s)`); // Get available sessions for cwd lookup const sessions = getSessionsCallback?.() || []; @@ -788,7 +1085,7 @@ export async function routeModeratorResponse( ) .join('\n'); - for (const participantName of mentions) { + for (const participantName of mentionsToSpawn) { console.log(`[GroupChat:Debug] --- Spawning participant: @${participantName} ---`); // Find the participant info @@ -835,11 +1132,16 @@ export async function routeModeratorResponse( // Get the group chat folder path for file access permissions const groupChatFolder = getGroupChatDir(groupChatId); + const worktreeSection = matchingSession?.worktreeBasePath + ? `## Git Worktree\n\nYour configured worktree base directory is: ${matchingSession.worktreeBasePath}\nCreate your git worktree under this directory.\n\n` + : ''; + const participantPrompt = groupChatParticipantRequestPrompt .replace(/\{\{PARTICIPANT_NAME\}\}/g, participantName) .replace(/\{\{GROUP_CHAT_NAME\}\}/g, updatedChat.name) .replace(/\{\{READ_ONLY_NOTE\}\}/g, readOnlyNote) .replace(/\{\{GROUP_CHAT_FOLDER\}\}/g, groupChatFolder) + .replace(/\{\{WORKTREE_BASE_PATH\}\}/g, worktreeSection) .replace(/\{\{HISTORY_CONTEXT\}\}/g, historyContext) .replace(/\{\{READ_ONLY_LABEL\}\}/g, readOnlyLabel) .replace(/\{\{MESSAGE\}\}/g, message) @@ -985,8 +1287,10 @@ export async function routeModeratorResponse( } } console.log(`[GroupChat:Debug] =================================================`); - } else if (mentions.length === 0) { - console.log(`[GroupChat:Debug] No participant @mentions found - moderator response is final`); + } else if (mentionsToSpawn.length === 0 && autoRunParticipants.length === 0) { + console.log( + `[GroupChat:Debug] No participant @mentions or autorun directives found - moderator response is final` + ); // Set state back to idle since no agents are being spawned groupChatEmitters.emitStateChange?.(groupChatId, 'idle'); console.log(`[GroupChat:Debug] Emitted state change: idle`); @@ -1214,14 +1518,36 @@ export async function spawnModeratorSynthesis( // Build participant context for potential follow-up @mentions // Use normalized names (spaces → hyphens) so moderator can @mention them properly + const synthSessionsList = getSessionsCallback?.() || []; const participantContext = chat.participants.length > 0 ? chat.participants - .map((p) => `- @${normalizeMentionName(p.name)} (${p.agentId} session)`) + .map((p) => { + const matchingSession = synthSessionsList.find( + (s) => mentionMatches(s.name, p.name) || s.name === p.name + ); + const worktreeNote = matchingSession?.worktreeBasePath + ? ` [worktree base: ${matchingSession.worktreeBasePath}]` + : ''; + return `- @${normalizeMentionName(p.name)} (${p.agentId} session)${worktreeNote}`; + }) .join('\n') : '(No agents currently in this group chat)'; - const synthesisPrompt = `${getModeratorSystemPrompt()} + // Get moderator settings for prompt customization + const synthModeratorSettings = getModeratorSettingsCallback?.() ?? { + standingInstructions: '', + conductorProfile: '', + }; + const synthBasePrompt = getModeratorSystemPrompt().replace( + '{{CONDUCTOR_PROFILE}}', + synthModeratorSettings.conductorProfile || '(No conductor profile set)' + ); + const synthStandingInstructions = synthModeratorSettings.standingInstructions + ? `\n\n## Standing Instructions\n\nThe following instructions apply to ALL group chat sessions. Follow them consistently:\n\n${synthModeratorSettings.standingInstructions}` + : ''; + + const synthesisPrompt = `${synthBasePrompt}${synthStandingInstructions} ${getModeratorSynthesisPrompt()} @@ -1385,11 +1711,16 @@ export async function respawnParticipantWithRecovery( const groupChatFolder = getGroupChatDir(groupChatId); // Build the recovery prompt - includes standard prompt plus recovery context + const recoveryWorktreeSection = matchingSession?.worktreeBasePath + ? `## Git Worktree\n\nYour configured worktree base directory is: ${matchingSession.worktreeBasePath}\nCreate your git worktree under this directory.\n\n` + : ''; + const basePrompt = groupChatParticipantRequestPrompt .replace(/\{\{PARTICIPANT_NAME\}\}/g, participantName) .replace(/\{\{GROUP_CHAT_NAME\}\}/g, chat.name) .replace(/\{\{READ_ONLY_NOTE\}\}/g, readOnlyNote) .replace(/\{\{GROUP_CHAT_FOLDER\}\}/g, groupChatFolder) + .replace(/\{\{WORKTREE_BASE_PATH\}\}/g, recoveryWorktreeSection) .replace(/\{\{HISTORY_CONTEXT\}\}/g, historyContext) .replace(/\{\{READ_ONLY_LABEL\}\}/g, readOnlyLabel) .replace( diff --git a/src/main/index.ts b/src/main/index.ts index 577f81530f..22f612bb36 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -66,6 +66,7 @@ import { setGetSessionsCallback, setGetCustomEnvVarsCallback, setGetAgentConfigCallback, + setGetModeratorSettingsCallback, setSshStore, setGetCustomShellPathCallback, markParticipantResponded, @@ -626,6 +627,8 @@ function setupIpcHandlers() { sshRemoteName, // Pass full SSH config for remote execution support sshRemoteConfig: s.sessionSshRemoteConfig, + autoRunFolderPath: s.autoRunFolderPath, + worktreeBasePath: s.worktreeConfig?.basePath, }; }); }); @@ -634,6 +637,12 @@ function setupIpcHandlers() { setGetCustomEnvVarsCallback(getCustomEnvVarsForAgent); setGetAgentConfigCallback(getAgentConfigForAgent); + // Set up callback for group chat router to get moderator standing instructions + conductor profile + setGetModeratorSettingsCallback(() => ({ + standingInstructions: (store.get('moderatorStandingInstructions', '') as string) || '', + conductorProfile: (store.get('conductorProfile', '') as string) || '', + })); + // Set up SSH store for group chat SSH remote execution support setSshStore(createSshRemoteStoreAdapter(store)); diff --git a/src/main/ipc/handlers/groupChat.ts b/src/main/ipc/handlers/groupChat.ts index a2a97622b3..87330f0978 100644 --- a/src/main/ipc/handlers/groupChat.ts +++ b/src/main/ipc/handlers/groupChat.ts @@ -100,6 +100,7 @@ export const groupChatEmitters: { state: ParticipantState ) => void; emitModeratorSessionIdChanged?: (groupChatId: string, sessionId: string) => void; + emitParticipantLiveOutput?: (groupChatId: string, participantName: string, chunk: string) => void; } = {}; // Helper to create handler options with consistent context @@ -872,5 +873,25 @@ Respond with ONLY the summary text, no additional commentary.`; } }; + /** + * Emit live output chunks from a participant to the renderer. + * Called as data streams in from participant processes. + */ + groupChatEmitters.emitParticipantLiveOutput = ( + groupChatId: string, + participantName: string, + chunk: string + ): void => { + const mainWindow = getMainWindow(); + if (isWebContentsAvailable(mainWindow)) { + mainWindow.webContents.send( + 'groupChat:participantLiveOutput', + groupChatId, + participantName, + chunk + ); + } + }; + logger.info('Registered Group Chat IPC handlers', LOG_CONTEXT); } diff --git a/src/main/preload/groupChat.ts b/src/main/preload/groupChat.ts index 692883c45b..0c8318515c 100644 --- a/src/main/preload/groupChat.ts +++ b/src/main/preload/groupChat.ts @@ -204,6 +204,15 @@ export function createGroupChatApi() { return () => ipcRenderer.removeListener('groupChat:participantState', handler); }, + onParticipantLiveOutput: ( + callback: (groupChatId: string, participantName: string, chunk: string) => void + ) => { + const handler = (_: any, groupChatId: string, participantName: string, chunk: string) => + callback(groupChatId, participantName, chunk); + ipcRenderer.on('groupChat:participantLiveOutput', handler); + return () => ipcRenderer.removeListener('groupChat:participantLiveOutput', handler); + }, + onModeratorSessionIdChanged: (callback: (groupChatId: string, sessionId: string) => void) => { const handler = (_: any, groupChatId: string, sessionId: string) => callback(groupChatId, sessionId); diff --git a/src/main/process-listeners/data-listener.ts b/src/main/process-listeners/data-listener.ts index b958029467..7130c45601 100644 --- a/src/main/process-listeners/data-listener.ts +++ b/src/main/process-listeners/data-listener.ts @@ -5,6 +5,7 @@ import type { ProcessManager } from '../process-manager'; import { GROUP_CHAT_PREFIX, type ProcessListenerDependencies } from './types'; +import { groupChatEmitters } from '../ipc/handlers/groupChat'; /** * Maximum buffer size per session (10MB). @@ -91,6 +92,12 @@ export function setupDataListener( `WARNING: Buffer size ${totalLength} exceeds ${MAX_BUFFER_SIZE} bytes for participant ${participantInfo.participantName}` ); } + // Emit live output chunk to renderer for peek display + groupChatEmitters.emitParticipantLiveOutput?.( + participantInfo.groupChatId, + participantInfo.participantName, + data + ); return; // Don't send to regular process:data handler } diff --git a/src/prompts/group-chat-moderator-system.md b/src/prompts/group-chat-moderator-system.md index 9e6f3da2d8..159b57aaeb 100644 --- a/src/prompts/group-chat-moderator-system.md +++ b/src/prompts/group-chat-moderator-system.md @@ -29,3 +29,22 @@ Your role is to: - If you need multiple rounds of work, keep @mentioning agents until the task is complete - Only return to the user when you have a complete, actionable answer - When you're done and ready to hand back to the user, provide a summary WITHOUT any @mentions + +## Auto Run Execution: + +- Use `!autorun @AgentName` to trigger execution of an agent's Auto Run documents +- The agent will process all unchecked tasks in their configured Auto Run folder +- Multiple agents can be triggered in parallel: + !autorun @Agent1 + !autorun @Agent2 +- Use this AFTER agents have created their implementation plans as Auto Run documents +- Do NOT combine !autorun with a regular @mention for the same agent in the same message + +## Commit & Switch Branch: + +- When the user sends `!commit`, instruct ALL participating agents to: + 1. Commit all staged and unstaged changes on their current branch with a descriptive commit message + 2. If the agent is working in a git worktree, remove the worktree (`git worktree remove `) and then checkout the feature branch in the main repository so the user can run it locally +- @mention each agent with clear, specific instructions +- After all agents respond, provide a summary with each agent's branch name and commit status +- If an agent reports conflicts or errors, relay them clearly to the user diff --git a/src/prompts/group-chat-participant-request.md b/src/prompts/group-chat-participant-request.md index 50c97b14f8..57b1e00b31 100644 --- a/src/prompts/group-chat-participant-request.md +++ b/src/prompts/group-chat-participant-request.md @@ -16,7 +16,7 @@ You have permission to read and write files in: The shared folder contains chat logs and can be used for collaborative file exchange between participants. -## Recent Chat History: +{{WORKTREE_BASE_PATH}}## Recent Chat History: {{HISTORY_CONTEXT}} diff --git a/src/renderer/components/GroupChatList.tsx b/src/renderer/components/GroupChatList.tsx index c413b4784d..7c0f85ba3b 100644 --- a/src/renderer/components/GroupChatList.tsx +++ b/src/renderer/components/GroupChatList.tsx @@ -231,7 +231,7 @@ export function GroupChatList({ if (showArchived && a.archived !== b.archived) { return a.archived ? 1 : -1; } - return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); + return (b.updatedAt ?? b.createdAt) - (a.updatedAt ?? a.createdAt); }); }, [groupChats, showArchived]); diff --git a/src/renderer/components/GroupChatParticipants.tsx b/src/renderer/components/GroupChatParticipants.tsx index 362db0fda8..27e9d12610 100644 --- a/src/renderer/components/GroupChatParticipants.tsx +++ b/src/renderer/components/GroupChatParticipants.tsx @@ -13,6 +13,7 @@ import { ParticipantCard } from './ParticipantCard'; import { formatShortcutKeys } from '../utils/shortcutFormatter'; import { buildParticipantColorMap } from '../utils/participantColors'; import { useResizablePanel } from '../hooks'; +import { useGroupChatStore } from '../stores/groupChatStore'; interface GroupChatParticipantsProps { theme: Theme; @@ -62,6 +63,8 @@ export function GroupChatParticipants({ side: 'right', }); + const participantLiveOutput = useGroupChatStore((s) => s.participantLiveOutput); + // Generate consistent colors for all participants (including "Moderator" for the moderator card) const participantColors = useMemo(() => { return buildParticipantColorMap(['Moderator', ...participants.map((p) => p.name)], theme); @@ -164,10 +167,11 @@ export function GroupChatParticipants({ key={participant.sessionId} theme={theme} participant={participant} - state={participantStates.get(participant.sessionId) || 'idle'} + state={participantStates.get(participant.name) || 'idle'} color={participantColors[participant.name]} groupChatId={groupChatId} onContextReset={handleContextReset} + liveOutput={participantLiveOutput.get(participant.name)} /> )) )} diff --git a/src/renderer/components/ParticipantCard.tsx b/src/renderer/components/ParticipantCard.tsx index 2aec2b916c..8987608d8f 100644 --- a/src/renderer/components/ParticipantCard.tsx +++ b/src/renderer/components/ParticipantCard.tsx @@ -5,8 +5,17 @@ * session ID, context usage, stats, and cost. */ -import { MessageSquare, Copy, Check, DollarSign, RotateCcw, Server } from 'lucide-react'; -import { useState, useCallback } from 'react'; +import { + MessageSquare, + Copy, + Check, + DollarSign, + RotateCcw, + Server, + Eye, + EyeOff, +} from 'lucide-react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import type { Theme, GroupChatParticipant, SessionState } from '../types'; import { getStatusColor } from '../utils/theme'; import { formatCost } from '../utils/formatters'; @@ -19,6 +28,7 @@ interface ParticipantCardProps { color?: string; groupChatId?: string; onContextReset?: (participantName: string) => void; + liveOutput?: string; } /** @@ -39,9 +49,26 @@ export function ParticipantCard({ color, groupChatId, onContextReset, + liveOutput, }: ParticipantCardProps): JSX.Element { const [copied, setCopied] = useState(false); const [isResetting, setIsResetting] = useState(false); + const [peekOpen, setPeekOpen] = useState(false); + const peekRef = useRef(null); + + // Auto-scroll peek output to bottom + useEffect(() => { + if (peekOpen && peekRef.current) { + peekRef.current.scrollTop = peekRef.current.scrollHeight; + } + }, [peekOpen, liveOutput]); + + // Close peek when participant goes idle (no more output) + useEffect(() => { + if (state !== 'busy' && state !== 'connecting') { + setPeekOpen(false); + } + }, [state]); // Use agent's session ID (clean GUID) when available, otherwise show pending const agentSessionId = participant.agentSessionId; @@ -237,6 +264,37 @@ export function ParticipantCard({ )} + + {/* Live output peek */} + {(state === 'busy' || state === 'connecting') && liveOutput && ( +
+ + {peekOpen && ( +
+							{liveOutput.length > 4096 ? liveOutput.slice(-4096) : liveOutput}
+						
+ )} +
+ )} ); } diff --git a/src/renderer/components/Settings/SettingsModal.tsx b/src/renderer/components/Settings/SettingsModal.tsx index a1d665dc0a..2aaa2a9b50 100644 --- a/src/renderer/components/Settings/SettingsModal.tsx +++ b/src/renderer/components/Settings/SettingsModal.tsx @@ -10,6 +10,7 @@ import { FlaskConical, Server, Monitor, + Users, } from 'lucide-react'; import { useSettings } from '../../hooks'; import type { Theme, LLMProvider } from '../../types'; @@ -45,6 +46,7 @@ interface SettingsModalProps { | 'theme' | 'notifications' | 'aicommands' + | 'groupchat' | 'ssh' | 'encore'; hasNoAgents?: boolean; @@ -92,6 +94,9 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro setSshRemoteIgnorePatterns, sshRemoteHonorGitignore, setSshRemoteHonorGitignore, + // Group Chat settings + moderatorStandingInstructions, + setModeratorStandingInstructions, } = useSettings(); const [activeTab, setActiveTab] = useState< @@ -102,6 +107,7 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro | 'theme' | 'notifications' | 'aicommands' + | 'groupchat' | 'ssh' | 'encore' >('general'); @@ -166,6 +172,7 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro | 'theme' | 'notifications' | 'aicommands' + | 'groupchat' | 'ssh' | 'encore' > = FEATURE_FLAGS.LLM_SETTINGS @@ -177,6 +184,7 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro 'theme', 'notifications', 'aicommands', + 'groupchat', 'ssh', 'encore', ] @@ -187,6 +195,7 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro 'theme', 'notifications', 'aicommands', + 'groupchat', 'ssh', 'encore', ]; @@ -391,6 +400,14 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro {activeTab === 'aicommands' && AI Commands} +