feat(runtime): improve provider delivery visibility
This commit is contained in:
parent
7138887a3b
commit
20c3194160
62 changed files with 2849 additions and 750 deletions
|
|
@ -93,17 +93,29 @@ const ruGuide: DefaultTheme.SidebarItem[] = [
|
|||
];
|
||||
|
||||
const rootNav: DefaultTheme.NavItem[] = [
|
||||
{ text: "Guide", link: "/guide/quickstart" },
|
||||
{ text: "Reference", link: "/reference/concepts" },
|
||||
{ text: "Troubleshooting", link: "/guide/troubleshooting" },
|
||||
{ text: "Download", link: downloadUrl, target: "_self" }
|
||||
{ text: "Guide", link: "/guide/quickstart", activeMatch: "^/guide/(?!troubleshooting(?:/|$))" },
|
||||
{ text: "Reference", link: "/reference/concepts", activeMatch: "^/reference/" },
|
||||
{
|
||||
text: "Troubleshooting",
|
||||
link: "/guide/troubleshooting",
|
||||
activeMatch: "^/guide/troubleshooting(?:/|$)"
|
||||
},
|
||||
{ text: "Download", link: downloadUrl, target: "_self", noIcon: true }
|
||||
];
|
||||
|
||||
const ruNav: DefaultTheme.NavItem[] = [
|
||||
{ text: "Руководство", link: "/ru/guide/quickstart" },
|
||||
{ text: "Справочник", link: "/ru/reference/concepts" },
|
||||
{ text: "Диагностика", link: "/ru/guide/troubleshooting" },
|
||||
{ text: "Скачать", link: ruDownloadUrl, target: "_self" }
|
||||
{
|
||||
text: "Руководство",
|
||||
link: "/ru/guide/quickstart",
|
||||
activeMatch: "^/ru/guide/(?!troubleshooting(?:/|$))"
|
||||
},
|
||||
{ text: "Справочник", link: "/ru/reference/concepts", activeMatch: "^/ru/reference/" },
|
||||
{
|
||||
text: "Диагностика",
|
||||
link: "/ru/guide/troubleshooting",
|
||||
activeMatch: "^/ru/guide/troubleshooting(?:/|$)"
|
||||
},
|
||||
{ text: "Скачать", link: ruDownloadUrl, target: "_self", noIcon: true }
|
||||
];
|
||||
|
||||
export default defineConfig({
|
||||
|
|
@ -173,12 +185,20 @@ export default defineConfig({
|
|||
}
|
||||
},
|
||||
themeConfig: {
|
||||
logo: "/logo-192.png",
|
||||
logo: {
|
||||
light: "/logo-192.png",
|
||||
dark: "/logo-192.png",
|
||||
alt: "Agent Teams"
|
||||
},
|
||||
siteTitle: "Agent Teams",
|
||||
outline: {
|
||||
level: [2, 3],
|
||||
label: "On this page"
|
||||
},
|
||||
externalLinkIcon: true,
|
||||
darkModeSwitchLabel: "Appearance",
|
||||
lightModeSwitchTitle: "Switch to light theme",
|
||||
darkModeSwitchTitle: "Switch to dark theme",
|
||||
search: {
|
||||
provider: "local",
|
||||
options: {
|
||||
|
|
@ -198,6 +218,14 @@ export default defineConfig({
|
|||
}
|
||||
}
|
||||
},
|
||||
lastUpdated: {
|
||||
text: "Last updated",
|
||||
formatOptions: {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
forceLocale: true
|
||||
}
|
||||
},
|
||||
nav: rootNav,
|
||||
sidebar: {
|
||||
"/ru/": ruGuide,
|
||||
|
|
@ -236,6 +264,9 @@ export default defineConfig({
|
|||
level: [2, 3],
|
||||
label: "На этой странице"
|
||||
},
|
||||
darkModeSwitchLabel: "Оформление",
|
||||
lightModeSwitchTitle: "Переключить на светлую тему",
|
||||
darkModeSwitchTitle: "Переключить на тёмную тему",
|
||||
search: {
|
||||
provider: "local",
|
||||
options: {
|
||||
|
|
@ -255,6 +286,14 @@ export default defineConfig({
|
|||
}
|
||||
}
|
||||
},
|
||||
lastUpdated: {
|
||||
text: "Обновлено",
|
||||
formatOptions: {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
forceLocale: true
|
||||
}
|
||||
},
|
||||
editLink: {
|
||||
pattern: `https://github.com/${REPO}/edit/main/landing/product-docs/:path`,
|
||||
text: "Редактировать на GitHub"
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -145,7 +145,7 @@
|
|||
"cronstrue": "^3.13.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"diff": "^8.0.3",
|
||||
"dompurify": "^3.3.1",
|
||||
"dompurify": "^3.4.2",
|
||||
"electron-updater": "^6.7.3",
|
||||
"fastify": "^5.8.5",
|
||||
"highlight.js": "^11.11.1",
|
||||
|
|
@ -153,7 +153,7 @@
|
|||
"isbinaryfile": "^6.0.0",
|
||||
"lucide-react": "^0.577.0",
|
||||
"mdast-util-to-hast": "^13.2.1",
|
||||
"mermaid": "^11.12.3",
|
||||
"mermaid": "^11.15.0",
|
||||
"motion": "12.38.0",
|
||||
"node-diff3": "^3.2.0",
|
||||
"node-pty": "^1.1.0",
|
||||
|
|
@ -177,7 +177,7 @@
|
|||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"unified": "^11.0.5",
|
||||
"yaml": "^2.8.2",
|
||||
"yaml": "^2.9.0",
|
||||
"yet-another-react-lightbox": "^3.29.1",
|
||||
"zustand": "^4.5.0"
|
||||
},
|
||||
|
|
@ -330,6 +330,10 @@
|
|||
},
|
||||
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"lodash-es": "^4.18.1",
|
||||
"uuid": "^11.1.1"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"electron",
|
||||
"node-pty",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,69 @@ export function truncateText(
|
|||
return lo > 0 ? text.slice(0, lo) + '...' : '...';
|
||||
}
|
||||
|
||||
function fitTextPrefix(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
maxWidth: number,
|
||||
font: string,
|
||||
): string {
|
||||
let lo = 0;
|
||||
let hi = text.length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi + 1) >> 1;
|
||||
if (measureTextCached(ctx, font, text.slice(0, mid)) <= maxWidth) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid - 1;
|
||||
}
|
||||
}
|
||||
return text.slice(0, lo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap text into a small fixed number of canvas lines.
|
||||
* The final line is truncated with "..." when the remaining text still overflows.
|
||||
*/
|
||||
export function wrapTextLines(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
text: string,
|
||||
maxWidth: number,
|
||||
font: string,
|
||||
maxLines: number,
|
||||
): string[] {
|
||||
const normalized = text.trim().replace(/\s+/g, ' ');
|
||||
if (!normalized || maxLines <= 0) return [];
|
||||
if (maxLines === 1) return [truncateText(ctx, normalized, maxWidth, font)];
|
||||
|
||||
const lines: string[] = [];
|
||||
let remaining = normalized;
|
||||
|
||||
while (remaining && lines.length < maxLines) {
|
||||
if (measureTextCached(ctx, font, remaining) <= maxWidth) {
|
||||
lines.push(remaining);
|
||||
break;
|
||||
}
|
||||
|
||||
if (lines.length === maxLines - 1) {
|
||||
lines.push(truncateText(ctx, remaining, maxWidth, font));
|
||||
break;
|
||||
}
|
||||
|
||||
const prefix = fitTextPrefix(ctx, remaining, maxWidth, font).trimEnd();
|
||||
if (!prefix) {
|
||||
lines.push(truncateText(ctx, remaining, maxWidth, font));
|
||||
break;
|
||||
}
|
||||
|
||||
const breakIndex = prefix.lastIndexOf(' ');
|
||||
const line = breakIndex > 0 ? prefix.slice(0, breakIndex) : prefix;
|
||||
lines.push(line);
|
||||
remaining = remaining.slice(line.length).trimStart();
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
// Pre-computed hex vertex unit offsets (avoids cos/sin per call)
|
||||
const HEX_COS: number[] = [];
|
||||
const HEX_SIN: number[] = [];
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import type { GraphNode } from '../ports/types';
|
||||
import { COLORS, getTaskStatusColor, getReviewStateColor } from '../constants/colors';
|
||||
import { TASK_PILL, MIN_VISIBLE_OPACITY, ANIM } from '../constants/canvas-constants';
|
||||
import { truncateText } from './draw-misc';
|
||||
import { truncateText, wrapTextLines } from './draw-misc';
|
||||
import { drawPillShell, drawPillStackLayer } from './draw-pill-shell';
|
||||
import { hexWithAlpha } from './render-cache';
|
||||
import type { KanbanZoneInfo } from '../layout/kanbanLayout';
|
||||
|
|
@ -153,7 +153,8 @@ function drawTaskPill(
|
|||
drawLiveTaskLogIndicator(ctx, -halfW + 8, -halfH + 8, time);
|
||||
}
|
||||
|
||||
// Subject (main title — large)
|
||||
// Subject (main title - up to two lines)
|
||||
let subjectLineCount = 0;
|
||||
if (node.sublabel) {
|
||||
ctx.font = `bold ${TASK_PILL.idFontSize}px sans-serif`;
|
||||
ctx.textAlign = 'left';
|
||||
|
|
@ -164,8 +165,13 @@ function drawTaskPill(
|
|||
node.reviewState !== 'approved' &&
|
||||
(node.reviewMode === 'manual' || (node.reviewMode === 'assigned' && !!node.reviewerName));
|
||||
const maxW = hasReviewChip ? w - 88 : w - 24;
|
||||
const subject = truncateText(ctx, node.sublabel, maxW, ctx.font);
|
||||
ctx.fillText(subject, textX, -12);
|
||||
const subjectLines = wrapTextLines(ctx, node.sublabel, maxW, ctx.font, 2);
|
||||
subjectLineCount = subjectLines.length;
|
||||
const titleStartY = subjectLines.length > 1 ? -16 : -12;
|
||||
const titleLineHeight = TASK_PILL.idFontSize + 1.5;
|
||||
subjectLines.forEach((line, index) => {
|
||||
ctx.fillText(line, textX, titleStartY + index * titleLineHeight);
|
||||
});
|
||||
}
|
||||
|
||||
// Display ID (secondary — small)
|
||||
|
|
@ -174,7 +180,7 @@ function drawTaskPill(
|
|||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillStyle = COLORS.textDim;
|
||||
ctx.fillText(displayId, -halfW + 10, 12);
|
||||
ctx.fillText(displayId, -halfW + 10, subjectLineCount > 1 ? 23 : 12);
|
||||
|
||||
// Approved badge: checkmark at right side
|
||||
if (node.reviewState === 'approved') {
|
||||
|
|
|
|||
342
pnpm-lock.yaml
342
pnpm-lock.yaml
|
|
@ -4,6 +4,10 @@ settings:
|
|||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
overrides:
|
||||
lodash-es: ^4.18.1
|
||||
uuid: ^11.1.1
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
|
|
@ -204,8 +208,8 @@ importers:
|
|||
specifier: ^8.0.3
|
||||
version: 8.0.3
|
||||
dompurify:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
specifier: ^3.4.2
|
||||
version: 3.4.2
|
||||
electron-updater:
|
||||
specifier: ^6.7.3
|
||||
version: 6.7.3
|
||||
|
|
@ -228,8 +232,8 @@ importers:
|
|||
specifier: ^13.2.1
|
||||
version: 13.2.1
|
||||
mermaid:
|
||||
specifier: ^11.12.3
|
||||
version: 11.12.3
|
||||
specifier: ^11.15.0
|
||||
version: 11.15.0
|
||||
motion:
|
||||
specifier: 12.38.0
|
||||
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
|
@ -295,13 +299,13 @@ importers:
|
|||
version: 3.5.0
|
||||
tailwindcss-animate:
|
||||
specifier: ^1.0.7
|
||||
version: 1.0.7(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 1.0.7(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0))
|
||||
unified:
|
||||
specifier: ^11.0.5
|
||||
version: 11.0.5
|
||||
yaml:
|
||||
specifier: ^2.8.2
|
||||
version: 2.8.2
|
||||
specifier: ^2.9.0
|
||||
version: 2.9.0
|
||||
yet-another-react-lightbox:
|
||||
specifier: ^3.29.1
|
||||
version: 3.29.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
|
|
@ -323,7 +327,7 @@ importers:
|
|||
version: 5.1.1(encoding@0.1.13)(rollup@4.60.0)
|
||||
'@tailwindcss/typography':
|
||||
specifier: ^0.5.19
|
||||
version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0))
|
||||
'@types/hast':
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4
|
||||
|
|
@ -401,7 +405,7 @@ importers:
|
|||
version: 3.0.6(eslint@9.39.2(jiti@1.21.7))
|
||||
eslint-plugin-tailwindcss:
|
||||
specifier: ^3.18.2
|
||||
version: 3.18.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 3.18.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0))
|
||||
globals:
|
||||
specifier: ^17.2.0
|
||||
version: 17.2.0
|
||||
|
|
@ -428,7 +432,7 @@ importers:
|
|||
version: 0.7.2(prettier@3.8.1)
|
||||
tailwindcss:
|
||||
specifier: ^3.4.1
|
||||
version: 3.4.19(tsx@4.21.0)(yaml@2.8.2)
|
||||
version: 3.4.19(tsx@4.21.0)(yaml@2.9.0)
|
||||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
|
|
@ -460,13 +464,13 @@ importers:
|
|||
version: 0.11.3(magicast@0.5.2)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)))
|
||||
'@vueuse/nuxt':
|
||||
specifier: ^10.11.1
|
||||
version: 10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
||||
version: 10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||
nuxt:
|
||||
specifier: ^3.20.2
|
||||
version: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)
|
||||
version: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)
|
||||
nuxt-icon:
|
||||
specifier: ^0.6.10
|
||||
version: 0.6.10(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
||||
version: 0.6.10(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||
pinia:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))
|
||||
|
|
@ -491,7 +495,7 @@ importers:
|
|||
version: 7.4.47
|
||||
'@nuxt/eslint':
|
||||
specifier: ^1.12.1
|
||||
version: 1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||
'@shikijs/transformers':
|
||||
specifier: 3.22.0
|
||||
version: 3.22.0
|
||||
|
|
@ -512,13 +516,13 @@ importers:
|
|||
version: 1.98.0
|
||||
tailwindcss:
|
||||
specifier: ^3.4.19
|
||||
version: 3.4.19(tsx@4.21.0)(yaml@2.8.2)
|
||||
version: 3.4.19(tsx@4.21.0)(yaml@2.9.0)
|
||||
vite-plugin-vuetify:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
||||
version: 2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
||||
vitepress:
|
||||
specifier: 2.0.0-alpha.17
|
||||
version: 2.0.0-alpha.17(@types/node@25.0.7)(axios@1.13.6)(change-case@5.4.4)(fuse.js@7.1.0)(idb-keyval@6.2.2)(jiti@2.6.1)(oxc-minify@0.117.0)(postcss@8.5.8)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
|
||||
version: 2.0.0-alpha.17(@types/node@25.0.7)(axios@1.13.6)(change-case@5.4.4)(fuse.js@7.1.0)(idb-keyval@6.2.2)(jiti@2.6.1)(oxc-minify@0.117.0)(postcss@8.5.8)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)
|
||||
vitepress-codeblock-collapse:
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(vue@3.5.30(typescript@5.9.3))
|
||||
|
|
@ -543,7 +547,7 @@ importers:
|
|||
version: 22.19.15
|
||||
tsup:
|
||||
specifier: ^8.5.1
|
||||
version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
|
||||
version: 8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)
|
||||
tsx:
|
||||
specifier: ^4.21.0
|
||||
version: 4.21.0
|
||||
|
|
@ -784,21 +788,9 @@ packages:
|
|||
'@braintree/sanitize-url@7.1.2':
|
||||
resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==}
|
||||
|
||||
'@chevrotain/cst-dts-gen@11.1.2':
|
||||
resolution: {integrity: sha512-XTsjvDVB5nDZBQB8o0o/0ozNelQtn2KrUVteIHSlPd2VAV2utEb6JzyCJaJ8tGxACR4RiBNWy5uYUHX2eji88Q==}
|
||||
|
||||
'@chevrotain/gast@11.1.2':
|
||||
resolution: {integrity: sha512-Z9zfXR5jNZb1Hlsd/p+4XWeUFugrHirq36bKzPWDSIacV+GPSVXdk+ahVWZTwjhNwofAWg/sZg58fyucKSQx5g==}
|
||||
|
||||
'@chevrotain/regexp-to-ast@11.1.2':
|
||||
resolution: {integrity: sha512-nMU3Uj8naWer7xpZTYJdxbAs6RIv/dxYzkYU8GSwgUtcAAlzjcPfX1w+RKRcYG8POlzMeayOQ/znfwxEGo5ulw==}
|
||||
|
||||
'@chevrotain/types@11.1.2':
|
||||
resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==}
|
||||
|
||||
'@chevrotain/utils@11.1.2':
|
||||
resolution: {integrity: sha512-4mudFAQ6H+MqBTfqLmU7G1ZwRzCLfJEooL/fsF6rCX5eePMbGhoy5n4g+G4vlh2muDcsCTJtL+uKbOzWxs5LHA==}
|
||||
|
||||
'@clack/core@1.1.0':
|
||||
resolution: {integrity: sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==}
|
||||
|
||||
|
|
@ -1979,8 +1971,8 @@ packages:
|
|||
'@mdi/js@7.4.47':
|
||||
resolution: {integrity: sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==}
|
||||
|
||||
'@mermaid-js/parser@1.0.0':
|
||||
resolution: {integrity: sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==}
|
||||
'@mermaid-js/parser@1.1.1':
|
||||
resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==}
|
||||
|
||||
'@miyaneee/rollup-plugin-json5@1.2.0':
|
||||
resolution: {integrity: sha512-JjTIaXZp9WzhUHpElrqPnl1AzBi/rvRs065F71+aTmlqvTMVkdbjZ8vfFl4nRlgJy+TPBw69ZK4pwFdmOAt4aA==}
|
||||
|
|
@ -4767,6 +4759,9 @@ packages:
|
|||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@upsetjs/venn.js@2.0.0':
|
||||
resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==}
|
||||
|
||||
'@vercel/nft@1.5.0':
|
||||
resolution: {integrity: sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA==}
|
||||
engines: {node: '>=20'}
|
||||
|
|
@ -5548,14 +5543,6 @@ packages:
|
|||
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
chevrotain-allstar@0.3.1:
|
||||
resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==}
|
||||
peerDependencies:
|
||||
chevrotain: ^11.0.0
|
||||
|
||||
chevrotain@11.1.2:
|
||||
resolution: {integrity: sha512-opLQzEVriiH1uUQ4Kctsd49bRoFDXGGSC4GUqj7pGyxM3RehRhvTlZJc1FL/Flew2p5uwxa1tUDWKzI4wNM8pg==}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
|
@ -6031,8 +6018,8 @@ packages:
|
|||
resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dagre-d3-es@7.0.13:
|
||||
resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==}
|
||||
dagre-d3-es@7.0.14:
|
||||
resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==}
|
||||
|
||||
damerau-levenshtein@1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
|
@ -6231,8 +6218,8 @@ packages:
|
|||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
dompurify@3.3.1:
|
||||
resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==}
|
||||
dompurify@3.4.2:
|
||||
resolution: {integrity: sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==}
|
||||
|
||||
domutils@3.2.2:
|
||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||
|
|
@ -6401,6 +6388,9 @@ packages:
|
|||
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-toolkit@1.46.1:
|
||||
resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==}
|
||||
|
||||
es6-error@4.1.1:
|
||||
resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==}
|
||||
|
||||
|
|
@ -7828,10 +7818,6 @@ packages:
|
|||
resolution: {integrity: sha512-2LOQnFKu3m0VxpE+5sb5+BRTSKrXmNxGgxVRiKwD9s5KQB1zID/FRXhtzeV7RT1L2GVpdEEAfVuclFOMGl1ikA==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
langium@4.2.1:
|
||||
resolution: {integrity: sha512-zu9QWmjpzJcomzdJQAHgDVhLGq5bLosVak1KVa40NzQHXfqr4eAHupvnPOVXEoLkg6Ocefvf/93d//SB7du4YQ==}
|
||||
engines: {node: '>=20.10.0', npm: '>=10.2.3'}
|
||||
|
||||
language-subtag-registry@0.3.23:
|
||||
resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
|
||||
|
||||
|
|
@ -7908,8 +7894,8 @@ packages:
|
|||
resolution: {integrity: sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg==}
|
||||
engines: {node: '>=20'}
|
||||
|
||||
lodash-es@4.17.23:
|
||||
resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==}
|
||||
lodash-es@4.18.1:
|
||||
resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==}
|
||||
|
||||
lodash.defaults@4.2.0:
|
||||
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
|
||||
|
|
@ -8122,8 +8108,8 @@ packages:
|
|||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
mermaid@11.12.3:
|
||||
resolution: {integrity: sha512-wN5ZSgJQIC+CHJut9xaKWsknLxaFBwCPwPkGTSUYrTiHORWvpT8RxGk849HPnpUAQ+/9BPRqYb80jTpearrHzQ==}
|
||||
mermaid@11.15.0:
|
||||
resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==}
|
||||
|
||||
micromark-core-commonmark@2.0.3:
|
||||
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
|
||||
|
|
@ -10651,8 +10637,8 @@ packages:
|
|||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
uuid@11.1.0:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
uuid@11.1.1:
|
||||
resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==}
|
||||
hasBin: true
|
||||
|
||||
vary@1.1.2:
|
||||
|
|
@ -10876,23 +10862,6 @@ packages:
|
|||
jsdom:
|
||||
optional: true
|
||||
|
||||
vscode-jsonrpc@8.2.0:
|
||||
resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
vscode-languageserver-protocol@3.17.5:
|
||||
resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==}
|
||||
|
||||
vscode-languageserver-textdocument@1.0.12:
|
||||
resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==}
|
||||
|
||||
vscode-languageserver-types@3.17.5:
|
||||
resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
|
||||
|
||||
vscode-languageserver@9.0.1:
|
||||
resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==}
|
||||
hasBin: true
|
||||
|
||||
vscode-uri@3.1.0:
|
||||
resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
|
||||
|
||||
|
|
@ -11125,6 +11094,11 @@ packages:
|
|||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yaml@2.9.0:
|
||||
resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==}
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -11504,23 +11478,8 @@ snapshots:
|
|||
|
||||
'@braintree/sanitize-url@7.1.2': {}
|
||||
|
||||
'@chevrotain/cst-dts-gen@11.1.2':
|
||||
dependencies:
|
||||
'@chevrotain/gast': 11.1.2
|
||||
'@chevrotain/types': 11.1.2
|
||||
lodash-es: 4.17.23
|
||||
|
||||
'@chevrotain/gast@11.1.2':
|
||||
dependencies:
|
||||
'@chevrotain/types': 11.1.2
|
||||
lodash-es: 4.17.23
|
||||
|
||||
'@chevrotain/regexp-to-ast@11.1.2': {}
|
||||
|
||||
'@chevrotain/types@11.1.2': {}
|
||||
|
||||
'@chevrotain/utils@11.1.2': {}
|
||||
|
||||
'@clack/core@1.1.0':
|
||||
dependencies:
|
||||
sisteransi: 1.0.5
|
||||
|
|
@ -12745,9 +12704,9 @@ snapshots:
|
|||
|
||||
'@mdi/js@7.4.47': {}
|
||||
|
||||
'@mermaid-js/parser@1.0.0':
|
||||
'@mermaid-js/parser@1.1.1':
|
||||
dependencies:
|
||||
langium: 4.2.1
|
||||
'@chevrotain/types': 11.1.2
|
||||
|
||||
'@miyaneee/rollup-plugin-json5@1.2.0(rollup@4.60.0)':
|
||||
dependencies:
|
||||
|
|
@ -12857,20 +12816,20 @@ snapshots:
|
|||
|
||||
'@nuxt/devalue@2.0.2': {}
|
||||
|
||||
'@nuxt/devtools-kit@1.7.0(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@nuxt/devtools-kit@1.7.0(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||
'@nuxt/schema': 3.21.2
|
||||
execa: 7.2.0
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@nuxt/devtools-kit@3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@nuxt/devtools-kit@3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
||||
execa: 8.0.1
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
|
|
@ -12885,9 +12844,9 @@ snapshots:
|
|||
pkg-types: 2.3.0
|
||||
semver: 7.7.4
|
||||
|
||||
'@nuxt/devtools@3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))':
|
||||
'@nuxt/devtools@3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||
'@nuxt/devtools-wizard': 3.2.4
|
||||
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
||||
'@vue/devtools-core': 8.1.0(vue@3.5.30(typescript@5.9.3))
|
||||
|
|
@ -12915,9 +12874,9 @@ snapshots:
|
|||
sirv: 3.0.2
|
||||
structured-clone-es: 2.0.0
|
||||
tinyglobby: 0.2.15
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-plugin-inspect: 11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vite-plugin-vue-tracer: 1.3.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
vite-plugin-inspect: 11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||
vite-plugin-vue-tracer: 1.3.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||
which: 6.0.1
|
||||
ws: 8.20.0
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -12966,10 +12925,10 @@ snapshots:
|
|||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@nuxt/eslint@1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@nuxt/eslint@1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
'@eslint/config-inspector': 1.5.0(eslint@9.39.2(jiti@2.6.1))
|
||||
'@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@nuxt/devtools-kit': 3.2.4(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||
'@nuxt/eslint-config': 1.15.2(@typescript-eslint/utils@8.57.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.30)(eslint-import-resolver-node@0.3.9)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@nuxt/eslint-plugin': 1.15.2(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
||||
|
|
@ -13045,7 +13004,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@nuxt/nitro-server@3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3)':
|
||||
'@nuxt/nitro-server@3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@nuxt/devalue': 2.0.2
|
||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||
|
|
@ -13063,7 +13022,7 @@ snapshots:
|
|||
klona: 2.0.6
|
||||
mocked-exports: 0.1.1
|
||||
nitropack: 2.13.2(encoding@0.1.13)(idb-keyval@6.2.2)
|
||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)
|
||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)
|
||||
ohash: 2.0.11
|
||||
pathe: 2.0.3
|
||||
pkg-types: 2.3.0
|
||||
|
|
@ -13128,12 +13087,12 @@ snapshots:
|
|||
rc9: 3.0.0
|
||||
std-env: 3.10.0
|
||||
|
||||
'@nuxt/vite-builder@3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.8.2)':
|
||||
'@nuxt/vite-builder@3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.9.0)':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||
'@rollup/plugin-replace': 6.0.3(rollup@4.60.0)
|
||||
'@vitejs/plugin-vue': 6.0.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue-jsx': 5.1.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue': 6.0.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue-jsx': 5.1.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||
autoprefixer: 10.5.0(postcss@8.5.8)
|
||||
consola: 3.4.2
|
||||
cssnano: 7.1.3(postcss@8.5.8)
|
||||
|
|
@ -13147,7 +13106,7 @@ snapshots:
|
|||
magic-string: 0.30.21
|
||||
mlly: 1.8.2
|
||||
mocked-exports: 0.1.1
|
||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)
|
||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)
|
||||
nypm: 0.6.5
|
||||
ohash: 2.0.11
|
||||
pathe: 2.0.3
|
||||
|
|
@ -13158,9 +13117,9 @@ snapshots:
|
|||
std-env: 4.0.0
|
||||
ufo: 1.6.3
|
||||
unenv: 2.0.0-rc.24
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-node: 5.3.0(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-plugin-checker: 0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
vite-node: 5.3.0(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
vite-plugin-checker: 0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||
vue: 3.5.30(typescript@5.9.3)
|
||||
vue-bundle-renderer: 2.2.0
|
||||
optionalDependencies:
|
||||
|
|
@ -14875,10 +14834,10 @@ snapshots:
|
|||
dependencies:
|
||||
defer-to-connect: 2.0.1
|
||||
|
||||
'@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@tailwindcss/typography@0.5.19(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0))':
|
||||
dependencies:
|
||||
postcss-selector-parser: 6.0.10
|
||||
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2)
|
||||
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.9.0)
|
||||
|
||||
'@tanstack/react-virtual@3.13.24(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
|
|
@ -15638,6 +15597,11 @@ snapshots:
|
|||
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
|
||||
optional: true
|
||||
|
||||
'@upsetjs/venn.js@2.0.0':
|
||||
optionalDependencies:
|
||||
d3-selection: 3.0.0
|
||||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
|
||||
'@vercel/nft@1.5.0(encoding@0.1.13)(rollup@4.60.0)':
|
||||
dependencies:
|
||||
'@mapbox/node-pre-gyp': 2.0.3(encoding@0.1.13)
|
||||
|
|
@ -15669,22 +15633,22 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))':
|
||||
'@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
|
||||
'@rolldown/pluginutils': 1.0.0-rc.11
|
||||
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.0)
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
vue: 3.5.30(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitejs/plugin-vue@6.0.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))':
|
||||
'@vitejs/plugin-vue@6.0.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.2
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
vue: 3.5.30(typescript@5.9.3)
|
||||
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.0.7)(happy-dom@20.0.2)(sass@1.98.0)(terser@5.46.0))':
|
||||
|
|
@ -15955,13 +15919,13 @@ snapshots:
|
|||
|
||||
'@vueuse/metadata@14.3.0': {}
|
||||
|
||||
'@vueuse/nuxt@10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))':
|
||||
'@vueuse/nuxt@10.11.1(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||
'@vueuse/core': 10.11.1(vue@3.5.30(typescript@5.9.3))
|
||||
'@vueuse/metadata': 10.11.1
|
||||
local-pkg: 0.5.1
|
||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2)
|
||||
nuxt: 3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0)
|
||||
vue-demi: 0.14.10(vue@3.5.30(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/composition-api'
|
||||
|
|
@ -16614,20 +16578,6 @@ snapshots:
|
|||
|
||||
check-error@2.1.3: {}
|
||||
|
||||
chevrotain-allstar@0.3.1(chevrotain@11.1.2):
|
||||
dependencies:
|
||||
chevrotain: 11.1.2
|
||||
lodash-es: 4.17.23
|
||||
|
||||
chevrotain@11.1.2:
|
||||
dependencies:
|
||||
'@chevrotain/cst-dts-gen': 11.1.2
|
||||
'@chevrotain/gast': 11.1.2
|
||||
'@chevrotain/regexp-to-ast': 11.1.2
|
||||
'@chevrotain/types': 11.1.2
|
||||
'@chevrotain/utils': 11.1.2
|
||||
lodash-es: 4.17.23
|
||||
|
||||
chokidar@3.6.0:
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
|
|
@ -17129,10 +17079,10 @@ snapshots:
|
|||
d3-transition: 3.0.1(d3-selection@3.0.0)
|
||||
d3-zoom: 3.0.0
|
||||
|
||||
dagre-d3-es@7.0.13:
|
||||
dagre-d3-es@7.0.14:
|
||||
dependencies:
|
||||
d3: 7.9.0
|
||||
lodash-es: 4.17.23
|
||||
lodash-es: 4.18.1
|
||||
|
||||
damerau-levenshtein@1.0.8: {}
|
||||
|
||||
|
|
@ -17298,7 +17248,7 @@ snapshots:
|
|||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
dompurify@3.3.1:
|
||||
dompurify@3.4.2:
|
||||
optionalDependencies:
|
||||
'@types/trusted-types': 2.0.7
|
||||
|
||||
|
|
@ -17566,6 +17516,8 @@ snapshots:
|
|||
is-date-object: 1.1.0
|
||||
is-symbol: 1.1.1
|
||||
|
||||
es-toolkit@1.46.1: {}
|
||||
|
||||
es6-error@4.1.1:
|
||||
optional: true
|
||||
|
||||
|
|
@ -17955,11 +17907,11 @@ snapshots:
|
|||
semver: 7.7.3
|
||||
typescript: 5.9.3
|
||||
|
||||
eslint-plugin-tailwindcss@3.18.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)):
|
||||
eslint-plugin-tailwindcss@3.18.2(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
fast-glob: 3.3.3
|
||||
postcss: 8.5.6
|
||||
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2)
|
||||
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.9.0)
|
||||
|
||||
eslint-plugin-unicorn@63.0.0(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
|
|
@ -19447,14 +19399,6 @@ snapshots:
|
|||
type-is: 2.0.1
|
||||
vary: 1.1.2
|
||||
|
||||
langium@4.2.1:
|
||||
dependencies:
|
||||
chevrotain: 11.1.2
|
||||
chevrotain-allstar: 0.3.1(chevrotain@11.1.2)
|
||||
vscode-languageserver: 9.0.1
|
||||
vscode-languageserver-textdocument: 1.0.12
|
||||
vscode-uri: 3.1.0
|
||||
|
||||
language-subtag-registry@0.3.23: {}
|
||||
|
||||
language-tags@1.0.9:
|
||||
|
|
@ -19505,7 +19449,7 @@ snapshots:
|
|||
nano-spawn: 2.0.0
|
||||
pidtree: 0.6.0
|
||||
string-argv: 0.3.2
|
||||
yaml: 2.8.2
|
||||
yaml: 2.9.0
|
||||
|
||||
listhen@1.9.0:
|
||||
dependencies:
|
||||
|
|
@ -19558,7 +19502,7 @@ snapshots:
|
|||
dependencies:
|
||||
p-locate: 6.0.0
|
||||
|
||||
lodash-es@4.17.23: {}
|
||||
lodash-es@4.18.1: {}
|
||||
|
||||
lodash.defaults@4.2.0: {}
|
||||
|
||||
|
|
@ -19904,28 +19848,29 @@ snapshots:
|
|||
|
||||
merge2@1.4.1: {}
|
||||
|
||||
mermaid@11.12.3:
|
||||
mermaid@11.15.0:
|
||||
dependencies:
|
||||
'@braintree/sanitize-url': 7.1.2
|
||||
'@iconify/utils': 3.1.0
|
||||
'@mermaid-js/parser': 1.0.0
|
||||
'@mermaid-js/parser': 1.1.1
|
||||
'@types/d3': 7.4.3
|
||||
'@upsetjs/venn.js': 2.0.0
|
||||
cytoscape: 3.33.1
|
||||
cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1)
|
||||
cytoscape-fcose: 2.2.0(cytoscape@3.33.1)
|
||||
d3: 7.9.0
|
||||
d3-sankey: 0.12.3
|
||||
dagre-d3-es: 7.0.13
|
||||
dagre-d3-es: 7.0.14
|
||||
dayjs: 1.11.19
|
||||
dompurify: 3.3.1
|
||||
dompurify: 3.4.2
|
||||
es-toolkit: 1.46.1
|
||||
katex: 0.16.33
|
||||
khroma: 2.1.0
|
||||
lodash-es: 4.17.23
|
||||
marked: 16.4.2
|
||||
roughjs: 4.6.6
|
||||
stylis: 4.3.6
|
||||
ts-dedent: 2.2.0
|
||||
uuid: 11.1.0
|
||||
uuid: 11.1.1
|
||||
|
||||
micromark-core-commonmark@2.0.3:
|
||||
dependencies:
|
||||
|
|
@ -20467,27 +20412,27 @@ snapshots:
|
|||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
nuxt-icon@0.6.10(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)):
|
||||
nuxt-icon@0.6.10(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@iconify/collections': 1.0.665
|
||||
'@iconify/vue': 4.3.0(vue@3.5.30(typescript@5.9.3))
|
||||
'@nuxt/devtools-kit': 1.7.0(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@nuxt/devtools-kit': 1.7.0(magicast@0.5.2)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
- vite
|
||||
- vue
|
||||
|
||||
nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2):
|
||||
nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0):
|
||||
dependencies:
|
||||
'@dxup/nuxt': 0.4.0(magicast@0.5.2)(typescript@5.9.3)
|
||||
'@nuxt/cli': 3.34.0(@nuxt/schema@3.21.2)(cac@6.7.14)(magicast@0.5.2)
|
||||
'@nuxt/devtools': 3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
||||
'@nuxt/devtools': 3.2.4(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||
'@nuxt/kit': 3.21.2(magicast@0.5.2)
|
||||
'@nuxt/nitro-server': 3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3)
|
||||
'@nuxt/nitro-server': 3.21.2(db0@0.3.4)(encoding@0.1.13)(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0))(typescript@5.9.3)
|
||||
'@nuxt/schema': 3.21.2
|
||||
'@nuxt/telemetry': 2.7.0(@nuxt/kit@3.21.2(magicast@0.5.2))
|
||||
'@nuxt/vite-builder': 3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.8.2)
|
||||
'@nuxt/vite-builder': 3.21.2(@types/node@25.0.7)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.2)(nuxt@3.21.2(@parcel/watcher@2.5.6)(@types/node@25.0.7)(@vue/compiler-sfc@3.5.30)(cac@6.7.14)(db0@0.3.4)(encoding@0.1.13)(eslint@9.39.2(jiti@2.6.1))(idb-keyval@6.2.2)(ioredis@5.10.1)(magicast@0.5.2)(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(yaml@2.9.0))(optionator@0.9.4)(rollup-plugin-visualizer@7.0.1(rollup@4.60.0))(rollup@4.60.0)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3))(yaml@2.9.0)
|
||||
'@unhead/vue': 2.1.12(vue@3.5.30(typescript@5.9.3))
|
||||
'@vue/shared': 3.5.30
|
||||
c12: 3.3.3(magicast@0.5.2)
|
||||
|
|
@ -21111,23 +21056,23 @@ snapshots:
|
|||
camelcase-css: 2.0.1
|
||||
postcss: 8.5.6
|
||||
|
||||
postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2):
|
||||
postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.9.0):
|
||||
dependencies:
|
||||
lilconfig: 3.1.3
|
||||
optionalDependencies:
|
||||
jiti: 1.21.7
|
||||
postcss: 8.5.6
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.2
|
||||
yaml: 2.9.0
|
||||
|
||||
postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.2):
|
||||
postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.9.0):
|
||||
dependencies:
|
||||
lilconfig: 3.1.3
|
||||
optionalDependencies:
|
||||
jiti: 2.6.1
|
||||
postcss: 8.5.8
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.2
|
||||
yaml: 2.9.0
|
||||
|
||||
postcss-merge-longhand@7.0.5(postcss@8.5.8):
|
||||
dependencies:
|
||||
|
|
@ -22461,11 +22406,11 @@ snapshots:
|
|||
|
||||
tailwind-merge@3.5.0: {}
|
||||
|
||||
tailwindcss-animate@1.0.7(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2)):
|
||||
tailwindcss-animate@1.0.7(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.2)
|
||||
tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.9.0)
|
||||
|
||||
tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2):
|
||||
tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.9.0):
|
||||
dependencies:
|
||||
'@alloc/quick-lru': 5.2.0
|
||||
arg: 5.0.2
|
||||
|
|
@ -22484,7 +22429,7 @@ snapshots:
|
|||
postcss: 8.5.6
|
||||
postcss-import: 15.1.0(postcss@8.5.6)
|
||||
postcss-js: 4.1.0(postcss@8.5.6)
|
||||
postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.8.2)
|
||||
postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.21.0)(yaml@2.9.0)
|
||||
postcss-nested: 6.2.0(postcss@8.5.6)
|
||||
postcss-selector-parser: 6.1.2
|
||||
resolve: 1.22.11
|
||||
|
|
@ -22655,7 +22600,7 @@ snapshots:
|
|||
|
||||
tsscmp@1.0.6: {}
|
||||
|
||||
tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2):
|
||||
tsup@8.5.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0):
|
||||
dependencies:
|
||||
bundle-require: 5.1.0(esbuild@0.27.2)
|
||||
cac: 6.7.14
|
||||
|
|
@ -22666,7 +22611,7 @@ snapshots:
|
|||
fix-dts-default-cjs-exports: 1.0.1
|
||||
joycon: 3.1.1
|
||||
picocolors: 1.1.1
|
||||
postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.2)
|
||||
postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.9.0)
|
||||
resolve-from: 5.0.0
|
||||
rollup: 4.55.1
|
||||
source-map: 0.7.6
|
||||
|
|
@ -23084,7 +23029,7 @@ snapshots:
|
|||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
uuid@11.1.0: {}
|
||||
uuid@11.1.1: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
|
|
@ -23110,15 +23055,15 @@ snapshots:
|
|||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
vite-dev-rpc@1.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-hot-client: 2.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
vite-hot-client: 2.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||
|
||||
vite-hot-client@2.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
vite-hot-client@2.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
|
||||
vite-node@3.2.4(@types/node@22.19.15)(sass@1.98.0)(terser@5.46.0):
|
||||
dependencies:
|
||||
|
|
@ -23156,13 +23101,13 @@ snapshots:
|
|||
- supports-color
|
||||
- terser
|
||||
|
||||
vite-node@5.3.0(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
vite-node@5.3.0(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
es-module-lexer: 2.0.0
|
||||
obug: 2.1.1
|
||||
pathe: 2.0.3
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
|
|
@ -23176,7 +23121,7 @@ snapshots:
|
|||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-plugin-checker@0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
vite-plugin-checker@0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
chokidar: 4.0.3
|
||||
|
|
@ -23185,14 +23130,14 @@ snapshots:
|
|||
picomatch: 4.0.3
|
||||
tiny-invariant: 1.3.3
|
||||
tinyglobby: 0.2.15
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
vscode-uri: 3.1.0
|
||||
optionalDependencies:
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
optionator: 0.9.4
|
||||
typescript: 5.9.3
|
||||
|
||||
vite-plugin-inspect@11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
vite-plugin-inspect@11.3.3(@nuxt/kit@4.4.2(magicast@0.5.2))(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3
|
||||
|
|
@ -23202,29 +23147,29 @@ snapshots:
|
|||
perfect-debounce: 2.1.0
|
||||
sirv: 3.0.2
|
||||
unplugin-utils: 0.3.1
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-dev-rpc: 1.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
vite-dev-rpc: 1.1.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 4.4.2(magicast@0.5.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-tracer@1.3.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)):
|
||||
vite-plugin-vue-tracer@1.3.0(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3)):
|
||||
dependencies:
|
||||
estree-walker: 3.0.3
|
||||
exsolve: 1.0.8
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
source-map-js: 1.2.1
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
vue: 3.5.30(typescript@5.9.3)
|
||||
|
||||
vite-plugin-vuetify@2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3):
|
||||
vite-plugin-vuetify@2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3):
|
||||
dependencies:
|
||||
'@vuetify/loader-shared': 2.1.2(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
||||
debug: 4.4.3
|
||||
upath: 2.0.1
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
vue: 3.5.30(typescript@5.9.3)
|
||||
vuetify: 3.12.3(typescript@5.9.3)(vite-plugin-vuetify@2.1.3)(vue@3.5.30(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -23252,7 +23197,7 @@ snapshots:
|
|||
sass: 1.98.0
|
||||
terser: 5.46.0
|
||||
|
||||
vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
|
||||
vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0):
|
||||
dependencies:
|
||||
esbuild: 0.27.4
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
|
|
@ -23267,7 +23212,7 @@ snapshots:
|
|||
sass: 1.98.0
|
||||
terser: 5.46.0
|
||||
tsx: 4.21.0
|
||||
yaml: 2.8.2
|
||||
yaml: 2.9.0
|
||||
|
||||
vitepress-codeblock-collapse@1.0.0(vue@3.5.30(typescript@5.9.3)):
|
||||
dependencies:
|
||||
|
|
@ -23292,7 +23237,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vitepress@2.0.0-alpha.17(@types/node@25.0.7)(axios@1.13.6)(change-case@5.4.4)(fuse.js@7.1.0)(idb-keyval@6.2.2)(jiti@2.6.1)(oxc-minify@0.117.0)(postcss@8.5.8)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2):
|
||||
vitepress@2.0.0-alpha.17(@types/node@25.0.7)(axios@1.13.6)(change-case@5.4.4)(fuse.js@7.1.0)(idb-keyval@6.2.2)(jiti@2.6.1)(oxc-minify@0.117.0)(postcss@8.5.8)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0):
|
||||
dependencies:
|
||||
'@docsearch/css': 4.6.3
|
||||
'@docsearch/js': 4.6.3
|
||||
|
|
@ -23302,7 +23247,7 @@ snapshots:
|
|||
'@shikijs/transformers': 3.22.0
|
||||
'@shikijs/types': 3.23.0
|
||||
'@types/markdown-it': 14.1.2
|
||||
'@vitejs/plugin-vue': 6.0.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))
|
||||
'@vitejs/plugin-vue': 6.0.5(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))
|
||||
'@vue/devtools-api': 8.1.1
|
||||
'@vue/shared': 3.5.30
|
||||
'@vueuse/core': 14.3.0(vue@3.5.30(typescript@5.9.3))
|
||||
|
|
@ -23311,7 +23256,7 @@ snapshots:
|
|||
mark.js: 8.11.1
|
||||
minisearch: 7.2.0
|
||||
shiki: 3.23.0
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite: 7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)
|
||||
vue: 3.5.30(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
oxc-minify: 0.117.0
|
||||
|
|
@ -23421,21 +23366,6 @@ snapshots:
|
|||
- supports-color
|
||||
- terser
|
||||
|
||||
vscode-jsonrpc@8.2.0: {}
|
||||
|
||||
vscode-languageserver-protocol@3.17.5:
|
||||
dependencies:
|
||||
vscode-jsonrpc: 8.2.0
|
||||
vscode-languageserver-types: 3.17.5
|
||||
|
||||
vscode-languageserver-textdocument@1.0.12: {}
|
||||
|
||||
vscode-languageserver-types@3.17.5: {}
|
||||
|
||||
vscode-languageserver@9.0.1:
|
||||
dependencies:
|
||||
vscode-languageserver-protocol: 3.17.5
|
||||
|
||||
vscode-uri@3.1.0: {}
|
||||
|
||||
vue-bundle-renderer@2.2.0:
|
||||
|
|
@ -23495,7 +23425,7 @@ snapshots:
|
|||
vue: 3.5.30(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
vite-plugin-vuetify: 2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
||||
vite-plugin-vuetify: 2.1.3(vite@7.3.1(@types/node@25.0.7)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))(vue@3.5.30(typescript@5.9.3))(vuetify@3.12.3)
|
||||
|
||||
w3c-keyname@2.2.8: {}
|
||||
|
||||
|
|
@ -23639,6 +23569,8 @@ snapshots:
|
|||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
yaml@2.9.0: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yargs-parser@22.0.0: {}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,27 @@
|
|||
{
|
||||
"version": "0.0.30",
|
||||
"sourceRef": "v0.0.30",
|
||||
"version": "0.0.31",
|
||||
"sourceRef": "v0.0.31",
|
||||
"sourceRepository": "777genius/agent_teams_orchestrator",
|
||||
"releaseRepository": "777genius/agent-teams-ai",
|
||||
"releaseTag": "v1.2.0",
|
||||
"assets": {
|
||||
"darwin-arm64": {
|
||||
"file": "agent-teams-runtime-darwin-arm64-v0.0.30.tar.gz",
|
||||
"file": "agent-teams-runtime-darwin-arm64-v0.0.31.tar.gz",
|
||||
"archiveKind": "tar.gz",
|
||||
"binaryName": "claude-multimodel"
|
||||
},
|
||||
"darwin-x64": {
|
||||
"file": "agent-teams-runtime-darwin-x64-v0.0.30.tar.gz",
|
||||
"file": "agent-teams-runtime-darwin-x64-v0.0.31.tar.gz",
|
||||
"archiveKind": "tar.gz",
|
||||
"binaryName": "claude-multimodel"
|
||||
},
|
||||
"linux-x64": {
|
||||
"file": "agent-teams-runtime-linux-x64-v0.0.30.tar.gz",
|
||||
"file": "agent-teams-runtime-linux-x64-v0.0.31.tar.gz",
|
||||
"archiveKind": "tar.gz",
|
||||
"binaryName": "claude-multimodel"
|
||||
},
|
||||
"win32-x64": {
|
||||
"file": "agent-teams-runtime-win32-x64-v0.0.30.zip",
|
||||
"file": "agent-teams-runtime-win32-x64-v0.0.31.zip",
|
||||
"archiveKind": "zip",
|
||||
"binaryName": "claude-multimodel.exe"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -770,8 +770,7 @@ class CodexAccountFeatureFacadeImpl implements CodexAccountFeatureFacade {
|
|||
}
|
||||
|
||||
private async loadApiKeyAvailability(): Promise<CodexApiKeyAvailabilityDto> {
|
||||
const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
|
||||
if (storedKey?.value.trim()) {
|
||||
if (await this.apiKeyService.hasPreferred('OPENAI_API_KEY')) {
|
||||
return {
|
||||
available: true,
|
||||
source: 'stored',
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export class ClaudeMemberTranscriptPreviewSource implements MemberLogPreviewSour
|
|||
[input.memberName],
|
||||
{
|
||||
forceRefresh: input.forceRefresh === true,
|
||||
includeTeamSubagentSessionDiscovery: false,
|
||||
}
|
||||
);
|
||||
const dedupedRefs = dedupeMemberLogRefs(refs);
|
||||
|
|
|
|||
|
|
@ -125,7 +125,6 @@ import {
|
|||
PluginCatalogService,
|
||||
PluginInstallationStateService,
|
||||
PluginInstallService,
|
||||
RUNTIME_MANAGED_API_KEY_ENV_VARS,
|
||||
SkillsCatalogService,
|
||||
SkillsMutationService,
|
||||
SkillsWatcherService,
|
||||
|
|
@ -1478,7 +1477,6 @@ async function initializeServices(): Promise<void> {
|
|||
phase: 'settings',
|
||||
message: 'Loading secure settings...',
|
||||
});
|
||||
await apiKeyService.syncProcessEnv(RUNTIME_MANAGED_API_KEY_ENV_VARS);
|
||||
// warmup() and ensureInstalled() are deferred to after window creation
|
||||
// (did-finish-load handler) to avoid thread pool contention at startup.
|
||||
httpServer = new HttpServer();
|
||||
|
|
|
|||
|
|
@ -28,10 +28,7 @@ import {
|
|||
} from '@preload/constants/ipcChannels';
|
||||
import { createLogger } from '@shared/utils/logger';
|
||||
|
||||
import {
|
||||
type ApiKeyService,
|
||||
RUNTIME_MANAGED_API_KEY_ENV_VARS,
|
||||
} from '../services/extensions/apikeys/ApiKeyService';
|
||||
import { type ApiKeyService } from '../services/extensions/apikeys/ApiKeyService';
|
||||
import { GitHubStarsService } from '../services/extensions/catalog/GitHubStarsService';
|
||||
|
||||
import type { ExtensionFacadeService } from '../services/extensions/ExtensionFacadeService';
|
||||
|
|
@ -396,12 +393,7 @@ async function handleApiKeysSave(
|
|||
): Promise<IpcResult<ApiKeyEntry>> {
|
||||
return wrapHandler('apiKeysSave', () => {
|
||||
if (!request) throw new Error('Request is required');
|
||||
return getApiKeyService()
|
||||
.save(request)
|
||||
.then(async (entry) => {
|
||||
await getApiKeyService().syncProcessEnv(RUNTIME_MANAGED_API_KEY_ENV_VARS);
|
||||
return entry;
|
||||
});
|
||||
return getApiKeyService().save(request);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -411,11 +403,7 @@ async function handleApiKeysDelete(
|
|||
): Promise<IpcResult<void>> {
|
||||
return wrapHandler('apiKeysDelete', () => {
|
||||
if (typeof id !== 'string' || !id) throw new Error('Key ID is required');
|
||||
return getApiKeyService()
|
||||
.delete(id)
|
||||
.then(async () => {
|
||||
await getApiKeyService().syncProcessEnv(RUNTIME_MANAGED_API_KEY_ENV_VARS);
|
||||
});
|
||||
return getApiKeyService().delete(id);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -215,9 +215,9 @@ import type {
|
|||
TeamCreateResponse,
|
||||
TeamFastMode,
|
||||
TeamGetDataOptions,
|
||||
TeamLaunchFailureDiagnosticsBundle,
|
||||
TeamLaunchRequest,
|
||||
TeamLaunchResponse,
|
||||
TeamLaunchFailureDiagnosticsBundle,
|
||||
TeamMemberActivityMeta,
|
||||
TeamMessageNotificationData,
|
||||
TeamProviderBackendId,
|
||||
|
|
|
|||
|
|
@ -51,14 +51,11 @@ const PBKDF2_ITERATIONS = 100_000;
|
|||
const PBKDF2_KEY_BYTES = 32;
|
||||
const PBKDF2_SALT = 'claude-apikey-storage-v1';
|
||||
|
||||
export const RUNTIME_MANAGED_API_KEY_ENV_VARS = ['GEMINI_API_KEY'] as const;
|
||||
|
||||
export class ApiKeyService {
|
||||
private readonly filePath: string;
|
||||
private cache: StoredApiKey[] | null = null;
|
||||
private aesKey: Buffer | null = null;
|
||||
private readonly reportedDecryptFailures = new Set<string>();
|
||||
private readonly originalProcessEnv = new Map<string, string | undefined>();
|
||||
|
||||
constructor(claudeDir?: string) {
|
||||
const baseDir = claudeDir ?? path.join(os.homedir(), '.claude');
|
||||
|
|
@ -185,6 +182,16 @@ export class ApiKeyService {
|
|||
};
|
||||
}
|
||||
|
||||
async hasPreferred(envVarName: string, projectPath?: string): Promise<boolean> {
|
||||
const keys = await this.readStore();
|
||||
const preferred = this.pickPreferredKey(
|
||||
keys.filter((key) => key.envVarName === envVarName),
|
||||
projectPath
|
||||
);
|
||||
|
||||
return Boolean(preferred?.encryptedValue);
|
||||
}
|
||||
|
||||
async getStorageStatus(): Promise<ApiKeyStorageStatus> {
|
||||
const secure = this.isSecureBackend();
|
||||
const backend = this.getBackendName();
|
||||
|
|
@ -204,31 +211,6 @@ export class ApiKeyService {
|
|||
};
|
||||
}
|
||||
|
||||
async syncProcessEnv(envVarNames: readonly string[]): Promise<void> {
|
||||
if (!envVarNames.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const envVarName of envVarNames) {
|
||||
if (!this.originalProcessEnv.has(envVarName)) {
|
||||
this.originalProcessEnv.set(envVarName, process.env[envVarName]);
|
||||
}
|
||||
|
||||
const nextValue = (await this.lookupPreferred(envVarName))?.value;
|
||||
if (nextValue && nextValue.trim().length > 0) {
|
||||
process.env[envVarName] = nextValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
const originalValue = this.originalProcessEnv.get(envVarName);
|
||||
if (typeof originalValue === 'string' && originalValue.length > 0) {
|
||||
process.env[envVarName] = originalValue;
|
||||
} else {
|
||||
delete process.env[envVarName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Encryption ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* Extension services barrel export.
|
||||
*/
|
||||
|
||||
export { ApiKeyService, RUNTIME_MANAGED_API_KEY_ENV_VARS } from './apikeys/ApiKeyService';
|
||||
export { ApiKeyService } from './apikeys/ApiKeyService';
|
||||
export { GitHubStarsService } from './catalog/GitHubStarsService';
|
||||
export { GlamaMcpEnrichmentService } from './catalog/GlamaMcpEnrichmentService';
|
||||
export { McpCatalogAggregator } from './catalog/McpCatalogAggregator';
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ async function buildManagementCliEnvForBinary(binaryPath: string): Promise<NodeJ
|
|||
const { env } = await buildProviderAwareCliEnv({
|
||||
binaryPath,
|
||||
connectionMode: 'augment',
|
||||
allowStoredApiKeyDecryption: false,
|
||||
});
|
||||
return env;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ interface OpenCodeRuntimeManifest {
|
|||
installedAt: string;
|
||||
}
|
||||
|
||||
interface PlatformCandidate {
|
||||
export interface PlatformCandidate {
|
||||
packageName: string;
|
||||
reason: string;
|
||||
}
|
||||
|
|
@ -98,6 +98,22 @@ export function resolveAppManagedOpenCodeRuntimeBinaryPath(): string | null {
|
|||
return isAbsoluteExistingFile(manifest?.binaryPath) ? manifest.binaryPath : null;
|
||||
}
|
||||
|
||||
export async function resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath(): Promise<string | null> {
|
||||
const binaryPath = resolveAppManagedOpenCodeRuntimeBinaryPath();
|
||||
if (!binaryPath) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
await execCli(binaryPath, ['--version'], {
|
||||
timeout: VERSION_TIMEOUT_MS,
|
||||
windowsHide: true,
|
||||
});
|
||||
return binaryPath;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getExecutableName(): string {
|
||||
return process.platform === 'win32' ? 'opencode.exe' : 'opencode';
|
||||
}
|
||||
|
|
@ -150,10 +166,12 @@ function isLinuxMuslRuntime(): boolean {
|
|||
return !header?.glibcVersionRuntime;
|
||||
}
|
||||
|
||||
function getPlatformCandidates(): PlatformCandidate[] {
|
||||
const arch = process.arch;
|
||||
const musl = isLinuxMuslRuntime();
|
||||
if (process.platform === 'darwin') {
|
||||
export function getOpenCodeRuntimePlatformCandidates(
|
||||
platform: NodeJS.Platform = process.platform,
|
||||
arch: string = process.arch,
|
||||
musl: boolean = isLinuxMuslRuntime()
|
||||
): PlatformCandidate[] {
|
||||
if (platform === 'darwin') {
|
||||
if (arch === 'arm64') return [{ packageName: 'opencode-darwin-arm64', reason: 'macOS arm64' }];
|
||||
if (arch === 'x64') {
|
||||
return [
|
||||
|
|
@ -162,7 +180,7 @@ function getPlatformCandidates(): PlatformCandidate[] {
|
|||
];
|
||||
}
|
||||
}
|
||||
if (process.platform === 'linux') {
|
||||
if (platform === 'linux') {
|
||||
if (arch === 'arm64') {
|
||||
return musl
|
||||
? [
|
||||
|
|
@ -191,7 +209,7 @@ function getPlatformCandidates(): PlatformCandidate[] {
|
|||
];
|
||||
}
|
||||
}
|
||||
if (process.platform === 'win32') {
|
||||
if (platform === 'win32') {
|
||||
if (arch === 'arm64')
|
||||
return [{ packageName: 'opencode-windows-arm64', reason: 'Windows arm64' }];
|
||||
if (arch === 'x64') {
|
||||
|
|
@ -201,7 +219,7 @@ function getPlatformCandidates(): PlatformCandidate[] {
|
|||
];
|
||||
}
|
||||
}
|
||||
throw new Error(`OpenCode app install is not supported on ${process.platform}/${arch}`);
|
||||
throw new Error(`OpenCode app install is not supported on ${platform}/${arch}`);
|
||||
}
|
||||
|
||||
async function fetchText(url: string): Promise<string> {
|
||||
|
|
@ -231,7 +249,7 @@ async function fetchPackageMetadata(
|
|||
return parsed;
|
||||
}
|
||||
|
||||
function verifyIntegrity(buffer: Buffer, integrity: string): void {
|
||||
export function verifyOpenCodeRuntimePackageIntegrity(buffer: Buffer, integrity: string): void {
|
||||
const match = /^sha512-([A-Za-z0-9+/=]+)$/.exec(integrity.trim());
|
||||
if (!match) {
|
||||
throw new Error('OpenCode package integrity is missing sha512 metadata');
|
||||
|
|
@ -320,7 +338,7 @@ function assertSafeTarPath(name: string): void {
|
|||
}
|
||||
}
|
||||
|
||||
function extractBinaryFromTarball(tarball: Buffer): Buffer {
|
||||
export function extractOpenCodeRuntimeBinaryFromTarball(tarball: Buffer): Buffer {
|
||||
const tar = gunzipSync(tarball, { maxOutputLength: MAX_BINARY_BYTES + 1024 * 1024 });
|
||||
const targetName = `package/bin/${getExecutableName()}`;
|
||||
let offset = 0;
|
||||
|
|
@ -479,7 +497,7 @@ export class OpenCodeRuntimeInstallerService {
|
|||
try {
|
||||
this.publishProgress({ phase: 'checking', detail: 'Resolving latest OpenCode package...' });
|
||||
const rootMetadata = await fetchPackageMetadata(ROOT_PACKAGE_NAME);
|
||||
const candidates = getPlatformCandidates();
|
||||
const candidates = getOpenCodeRuntimePlatformCandidates();
|
||||
const optionalDependencies = rootMetadata.optionalDependencies ?? {};
|
||||
const selected = candidates.find((candidate) => optionalDependencies[candidate.packageName]);
|
||||
if (!selected) {
|
||||
|
|
@ -498,10 +516,10 @@ export class OpenCodeRuntimeInstallerService {
|
|||
const tarball = await downloadTarball(platformMetadata.dist!.tarball!, (progress) => {
|
||||
this.publishProgress(progress);
|
||||
});
|
||||
verifyIntegrity(tarball, platformMetadata.dist!.integrity!);
|
||||
verifyOpenCodeRuntimePackageIntegrity(tarball, platformMetadata.dist!.integrity!);
|
||||
|
||||
this.publishProgress({ phase: 'installing', detail: 'Extracting OpenCode binary...' });
|
||||
const binary = extractBinaryFromTarball(tarball);
|
||||
const binary = extractOpenCodeRuntimeBinaryFromTarball(tarball);
|
||||
const runtimeRoot = getRuntimeRootPath();
|
||||
const tempDir = path.join(runtimeRoot, `installing-${process.pid}-${randomUUID()}`);
|
||||
const versionDir = path.join(
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export class PtyTerminalService {
|
|||
const { env } = await buildProviderAwareCliEnv({
|
||||
env: options?.env,
|
||||
connectionMode: 'augment',
|
||||
allowStoredApiKeyDecryption: false,
|
||||
});
|
||||
const shell =
|
||||
options?.command ??
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ const logger = createLogger('ClaudeMultimodelBridgeService');
|
|||
|
||||
const PROVIDER_STATUS_TIMEOUT_MS = 10_000;
|
||||
const PROVIDER_MODELS_TIMEOUT_MS = 10_000;
|
||||
const PROVIDER_STATUS_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
||||
const PROVIDER_MODELS_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
interface RuntimeExtensionCapabilityResponse {
|
||||
status?: 'supported' | 'read-only' | 'unsupported';
|
||||
|
|
@ -82,6 +84,7 @@ interface RuntimeProviderModelCatalogItemResponse {
|
|||
source?: 'anthropic-models-api' | 'app-server' | 'static-fallback';
|
||||
badgeLabel?: string | null;
|
||||
statusMessage?: string | null;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
interface RuntimeProviderModelCatalogResponse {
|
||||
|
|
@ -478,6 +481,21 @@ function collectRuntimeReasoningEfforts(values?: string[]): CliProviderReasoning
|
|||
);
|
||||
}
|
||||
|
||||
function mapRuntimeProviderModelMetadata(
|
||||
metadata?: Record<string, unknown> | null
|
||||
): NonNullable<CliProviderStatus['modelCatalog']>['models'][number]['metadata'] {
|
||||
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
||||
return null;
|
||||
}
|
||||
const context = metadata.context;
|
||||
return {
|
||||
cost: metadata.cost ?? null,
|
||||
context: typeof context === 'number' && Number.isFinite(context) ? context : null,
|
||||
limits: metadata.limits ?? null,
|
||||
free: metadata.free === true,
|
||||
};
|
||||
}
|
||||
|
||||
function mapRuntimeProviderModelCatalog(
|
||||
providerId: CliProviderId,
|
||||
modelCatalog?: RuntimeProviderModelCatalogResponse | null
|
||||
|
|
@ -540,6 +558,7 @@ function mapRuntimeProviderModelCatalog(
|
|||
source: itemSource,
|
||||
badgeLabel: model.badgeLabel ?? null,
|
||||
statusMessage: model.statusMessage ?? null,
|
||||
metadata: mapRuntimeProviderModelMetadata(model.metadata),
|
||||
},
|
||||
];
|
||||
}) ?? [];
|
||||
|
|
@ -605,14 +624,18 @@ export class ClaudeMultimodelBridgeService {
|
|||
private async buildCliEnv(
|
||||
binaryPath: string
|
||||
): Promise<Awaited<ReturnType<typeof buildProviderAwareCliEnv>>> {
|
||||
return buildProviderAwareCliEnv({ binaryPath });
|
||||
return buildProviderAwareCliEnv({ binaryPath, allowStoredApiKeyDecryption: false });
|
||||
}
|
||||
|
||||
private async buildProviderCliEnv(
|
||||
binaryPath: string,
|
||||
providerId: CliProviderId
|
||||
): Promise<Awaited<ReturnType<typeof buildProviderAwareCliEnv>>> {
|
||||
return buildProviderAwareCliEnv({ binaryPath, providerId });
|
||||
return buildProviderAwareCliEnv({
|
||||
binaryPath,
|
||||
providerId,
|
||||
allowStoredApiKeyDecryption: false,
|
||||
});
|
||||
}
|
||||
|
||||
private isUnifiedRuntimeUnsupported(error: unknown): boolean {
|
||||
|
|
@ -769,6 +792,7 @@ export class ClaudeMultimodelBridgeService {
|
|||
['runtime', 'status', '--json', '--provider', providerId],
|
||||
{
|
||||
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
||||
maxBuffer: PROVIDER_STATUS_MAX_BUFFER_BYTES,
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
|
@ -850,6 +874,7 @@ export class ClaudeMultimodelBridgeService {
|
|||
['runtime', 'verify', '--json', '--provider', 'opencode'],
|
||||
{
|
||||
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
||||
maxBuffer: PROVIDER_STATUS_MAX_BUFFER_BYTES,
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
|
@ -1048,6 +1073,7 @@ export class ClaudeMultimodelBridgeService {
|
|||
['model', 'list', '--json', '--provider', 'all'],
|
||||
{
|
||||
timeout: PROVIDER_MODELS_TIMEOUT_MS,
|
||||
maxBuffer: PROVIDER_MODELS_MAX_BUFFER_BYTES,
|
||||
env,
|
||||
}
|
||||
);
|
||||
|
|
@ -1119,6 +1145,7 @@ export class ClaudeMultimodelBridgeService {
|
|||
try {
|
||||
const { stdout } = await execCli(binaryPath, ['runtime', 'status', '--json'], {
|
||||
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
||||
maxBuffer: PROVIDER_STATUS_MAX_BUFFER_BYTES,
|
||||
env,
|
||||
});
|
||||
const parsed = extractJsonObject<UnifiedRuntimeStatusResponse>(stdout);
|
||||
|
|
@ -1145,10 +1172,12 @@ export class ClaudeMultimodelBridgeService {
|
|||
const [statusResult, modelsResult] = await Promise.allSettled([
|
||||
execCli(binaryPath, ['auth', 'status', '--json', '--provider', 'all'], {
|
||||
timeout: PROVIDER_STATUS_TIMEOUT_MS,
|
||||
maxBuffer: PROVIDER_STATUS_MAX_BUFFER_BYTES,
|
||||
env,
|
||||
}),
|
||||
execCli(binaryPath, ['model', 'list', '--json', '--provider', 'all'], {
|
||||
timeout: PROVIDER_MODELS_TIMEOUT_MS,
|
||||
maxBuffer: PROVIDER_MODELS_MAX_BUFFER_BYTES,
|
||||
env,
|
||||
}),
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -193,6 +193,7 @@ export class CliProviderModelAvailabilityService {
|
|||
cliEnvPromise: buildProviderAwareCliEnv({
|
||||
binaryPath: context.binaryPath,
|
||||
providerId: context.provider.providerId,
|
||||
allowStoredApiKeyDecryption: false,
|
||||
}).then((result) => ({
|
||||
env: result.env,
|
||||
providerArgs: result.providerArgs ?? [],
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ type ExternalCredential = {
|
|||
value: string;
|
||||
} | null;
|
||||
|
||||
interface StoredApiKeyAccessOptions {
|
||||
allowStoredApiKeyDecryption?: boolean;
|
||||
}
|
||||
|
||||
const PROVIDER_CAPABILITIES: Record<
|
||||
CliProviderId,
|
||||
Pick<CliProviderConnectionInfo, 'supportsOAuth' | 'supportsApiKey' | 'configurableAuthModes'>
|
||||
|
|
@ -258,7 +262,8 @@ export class ProviderConnectionService {
|
|||
async applyConfiguredConnectionEnv(
|
||||
env: NodeJS.ProcessEnv,
|
||||
providerId: CliProviderId,
|
||||
runtimeBackendOverride?: string | null
|
||||
runtimeBackendOverride?: string | null,
|
||||
options?: StoredApiKeyAccessOptions
|
||||
): Promise<NodeJS.ProcessEnv> {
|
||||
if (providerId === 'anthropic') {
|
||||
const authMode = this.getConfiguredAuthMode(providerId);
|
||||
|
|
@ -272,7 +277,7 @@ export class ProviderConnectionService {
|
|||
return env;
|
||||
}
|
||||
|
||||
const storedKey = await this.apiKeyService.lookupPreferred('ANTHROPIC_API_KEY');
|
||||
const storedKey = await this.lookupStoredApiKeyValue('ANTHROPIC_API_KEY', options);
|
||||
if (storedKey?.value.trim()) {
|
||||
env.ANTHROPIC_API_KEY = storedKey.value;
|
||||
delete env.ANTHROPIC_AUTH_TOKEN;
|
||||
|
|
@ -288,6 +293,14 @@ export class ProviderConnectionService {
|
|||
return env;
|
||||
}
|
||||
|
||||
if (providerId === 'gemini') {
|
||||
const storedKey = await this.lookupStoredApiKeyValue('GEMINI_API_KEY', options);
|
||||
if (storedKey?.value.trim()) {
|
||||
env.GEMINI_API_KEY = storedKey.value;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
if (providerId !== 'codex') {
|
||||
return env;
|
||||
}
|
||||
|
|
@ -310,7 +323,7 @@ export class ProviderConnectionService {
|
|||
return env;
|
||||
}
|
||||
|
||||
const resolvedApiKey = await this.resolveCodexApiKeyValue(env, runtimeBackendOverride);
|
||||
const resolvedApiKey = await this.resolveCodexApiKeyValue(env, runtimeBackendOverride, options);
|
||||
if (readiness.effectiveAuthMode === 'api_key' && resolvedApiKey) {
|
||||
env.OPENAI_API_KEY = resolvedApiKey;
|
||||
env[CODEX_NATIVE_API_KEY_ENV_VAR] = resolvedApiKey;
|
||||
|
|
@ -327,10 +340,13 @@ export class ProviderConnectionService {
|
|||
return env;
|
||||
}
|
||||
|
||||
async applyAllConfiguredConnectionEnv(env: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv> {
|
||||
async applyAllConfiguredConnectionEnv(
|
||||
env: NodeJS.ProcessEnv,
|
||||
options?: StoredApiKeyAccessOptions
|
||||
): Promise<NodeJS.ProcessEnv> {
|
||||
let nextEnv = env;
|
||||
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode'] as const) {
|
||||
nextEnv = await this.applyConfiguredConnectionEnv(nextEnv, providerId);
|
||||
nextEnv = await this.applyConfiguredConnectionEnv(nextEnv, providerId, undefined, options);
|
||||
}
|
||||
return nextEnv;
|
||||
}
|
||||
|
|
@ -338,20 +354,29 @@ export class ProviderConnectionService {
|
|||
async augmentConfiguredConnectionEnv(
|
||||
env: NodeJS.ProcessEnv,
|
||||
providerId: CliProviderId,
|
||||
runtimeBackendOverride?: string | null
|
||||
runtimeBackendOverride?: string | null,
|
||||
options?: StoredApiKeyAccessOptions
|
||||
): Promise<NodeJS.ProcessEnv> {
|
||||
if (providerId === 'anthropic') {
|
||||
if (this.getConfiguredAuthMode(providerId) !== 'api_key') {
|
||||
return env;
|
||||
}
|
||||
|
||||
const storedKey = await this.apiKeyService.lookupPreferred('ANTHROPIC_API_KEY');
|
||||
const storedKey = await this.lookupStoredApiKeyValue('ANTHROPIC_API_KEY', options);
|
||||
if (storedKey?.value.trim()) {
|
||||
env.ANTHROPIC_API_KEY = storedKey.value;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
if (providerId === 'gemini') {
|
||||
const storedKey = await this.lookupStoredApiKeyValue('GEMINI_API_KEY', options);
|
||||
if (storedKey?.value.trim()) {
|
||||
env.GEMINI_API_KEY = storedKey.value;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
if (providerId !== 'codex') {
|
||||
return env;
|
||||
}
|
||||
|
|
@ -374,7 +399,7 @@ export class ProviderConnectionService {
|
|||
return env;
|
||||
}
|
||||
|
||||
const resolvedApiKey = await this.resolveCodexApiKeyValue(env, runtimeBackendOverride);
|
||||
const resolvedApiKey = await this.resolveCodexApiKeyValue(env, runtimeBackendOverride, options);
|
||||
if (readiness.effectiveAuthMode === 'api_key' && resolvedApiKey) {
|
||||
env.OPENAI_API_KEY = resolvedApiKey;
|
||||
env[CODEX_NATIVE_API_KEY_ENV_VAR] = resolvedApiKey;
|
||||
|
|
@ -386,10 +411,13 @@ export class ProviderConnectionService {
|
|||
return env;
|
||||
}
|
||||
|
||||
async augmentAllConfiguredConnectionEnv(env: NodeJS.ProcessEnv): Promise<NodeJS.ProcessEnv> {
|
||||
async augmentAllConfiguredConnectionEnv(
|
||||
env: NodeJS.ProcessEnv,
|
||||
options?: StoredApiKeyAccessOptions
|
||||
): Promise<NodeJS.ProcessEnv> {
|
||||
let nextEnv = env;
|
||||
for (const providerId of ['anthropic', 'codex', 'gemini', 'opencode'] as const) {
|
||||
nextEnv = await this.augmentConfiguredConnectionEnv(nextEnv, providerId);
|
||||
nextEnv = await this.augmentConfiguredConnectionEnv(nextEnv, providerId, undefined, options);
|
||||
}
|
||||
return nextEnv;
|
||||
}
|
||||
|
|
@ -408,8 +436,7 @@ export class ProviderConnectionService {
|
|||
return null;
|
||||
}
|
||||
|
||||
const storedKey = await this.apiKeyService.lookupPreferred('ANTHROPIC_API_KEY');
|
||||
if (storedKey?.value.trim()) {
|
||||
if (await this.hasStoredApiKey('ANTHROPIC_API_KEY')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -654,7 +681,7 @@ export class ProviderConnectionService {
|
|||
|
||||
async getConnectionInfo(providerId: CliProviderId): Promise<CliProviderConnectionInfo> {
|
||||
const capabilities = PROVIDER_CAPABILITIES[providerId];
|
||||
const storedApiKey = await this.getStoredApiKey(providerId);
|
||||
const hasStoredApiKey = await this.hasStoredProviderApiKey(providerId);
|
||||
const externalCredential = this.getExternalCredential(providerId);
|
||||
const codexSnapshot = providerId === 'codex' ? await this.getCodexAccountSnapshot() : null;
|
||||
const configurableAuthModes = capabilities.configurableAuthModes;
|
||||
|
|
@ -665,11 +692,11 @@ export class ProviderConnectionService {
|
|||
const apiKeyConfigured =
|
||||
providerId === 'codex'
|
||||
? (codexSnapshot?.apiKey.available ?? false)
|
||||
: Boolean(storedApiKey?.value.trim() || externalCredential?.value.trim());
|
||||
: Boolean(hasStoredApiKey || externalCredential?.value.trim());
|
||||
const apiKeySource =
|
||||
providerId === 'codex'
|
||||
? (codexSnapshot?.apiKey.source ?? null)
|
||||
: storedApiKey?.value.trim()
|
||||
: hasStoredApiKey
|
||||
? 'stored'
|
||||
: externalCredential?.value.trim()
|
||||
? 'environment'
|
||||
|
|
@ -677,7 +704,7 @@ export class ProviderConnectionService {
|
|||
const apiKeySourceLabel =
|
||||
providerId === 'codex'
|
||||
? (codexSnapshot?.apiKey.sourceLabel ?? null)
|
||||
: storedApiKey?.value.trim()
|
||||
: hasStoredApiKey
|
||||
? 'Stored in app'
|
||||
: (externalCredential?.label ?? null);
|
||||
|
||||
|
|
@ -709,11 +736,33 @@ export class ProviderConnectionService {
|
|||
};
|
||||
}
|
||||
|
||||
private async getStoredApiKey(
|
||||
providerId: CliProviderId
|
||||
): Promise<{ envVarName: string; value: string } | null> {
|
||||
private async hasStoredProviderApiKey(providerId: CliProviderId): Promise<boolean> {
|
||||
const envVarName = PROVIDER_API_KEY_ENV_VARS[providerId];
|
||||
if (!envVarName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.hasStoredApiKey(envVarName);
|
||||
}
|
||||
|
||||
private async hasStoredApiKey(envVarName: string): Promise<boolean> {
|
||||
const service = this.apiKeyService as ApiKeyService & {
|
||||
hasPreferred?: (envVarName: string) => Promise<boolean>;
|
||||
};
|
||||
|
||||
if (typeof service.hasPreferred === 'function') {
|
||||
return service.hasPreferred(envVarName);
|
||||
}
|
||||
|
||||
const storedKey = await service.lookupPreferred(envVarName);
|
||||
return Boolean(storedKey?.value.trim());
|
||||
}
|
||||
|
||||
private async lookupStoredApiKeyValue(
|
||||
envVarName: string,
|
||||
options?: StoredApiKeyAccessOptions
|
||||
): Promise<{ envVarName: string; value: string } | null> {
|
||||
if (options?.allowStoredApiKeyDecryption === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -736,17 +785,17 @@ export class ProviderConnectionService {
|
|||
(this.configManager.getConfig().providerConnections.codex.preferredAuthMode as
|
||||
| CodexAccountAuthMode
|
||||
| undefined) ?? 'auto';
|
||||
const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
|
||||
const hasStoredOpenAiKey = await this.hasStoredApiKey('OPENAI_API_KEY');
|
||||
const externalCredential = this.getExternalCredential('codex');
|
||||
const apiKeyAvailable = Boolean(storedKey?.value.trim() || externalCredential?.value.trim());
|
||||
const apiKeyAvailable = Boolean(hasStoredOpenAiKey || externalCredential?.value.trim());
|
||||
const apiKey = {
|
||||
available: apiKeyAvailable,
|
||||
source: storedKey?.value.trim()
|
||||
source: hasStoredOpenAiKey
|
||||
? 'stored'
|
||||
: externalCredential?.value.trim()
|
||||
? 'environment'
|
||||
: null,
|
||||
sourceLabel: storedKey?.value.trim() ? 'Stored in app' : (externalCredential?.label ?? null),
|
||||
sourceLabel: hasStoredOpenAiKey ? 'Stored in app' : (externalCredential?.label ?? null),
|
||||
} satisfies CodexAccountSnapshotDto['apiKey'];
|
||||
const readiness = evaluateCodexLaunchReadiness({
|
||||
preferredAuthMode,
|
||||
|
|
@ -786,10 +835,11 @@ export class ProviderConnectionService {
|
|||
|
||||
private async resolveCodexApiKeyValue(
|
||||
env: NodeJS.ProcessEnv,
|
||||
runtimeBackendOverride?: string | null
|
||||
runtimeBackendOverride?: string | null,
|
||||
options?: StoredApiKeyAccessOptions
|
||||
): Promise<string | null> {
|
||||
const codexRuntimeBackend = this.getConfiguredCodexRuntimeBackend(runtimeBackendOverride);
|
||||
const storedKey = await this.apiKeyService.lookupPreferred('OPENAI_API_KEY');
|
||||
const storedKey = await this.lookupStoredApiKeyValue('OPENAI_API_KEY', options);
|
||||
const existingOpenAiKey =
|
||||
typeof env.OPENAI_API_KEY === 'string' && env.OPENAI_API_KEY.trim()
|
||||
? env.OPENAI_API_KEY
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { getCachedShellEnv } from '@main/utils/shellEnv';
|
||||
|
||||
import { resolveAppManagedOpenCodeRuntimeBinaryPath } from '../infrastructure/OpenCodeRuntimeInstallerService';
|
||||
import { resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath } from '../infrastructure/OpenCodeRuntimeInstallerService';
|
||||
|
||||
import { buildRuntimeBaseEnv } from './buildRuntimeBaseEnv';
|
||||
import { providerConnectionService } from './ProviderConnectionService';
|
||||
|
|
@ -16,6 +16,7 @@ export interface ProviderAwareCliEnvOptions {
|
|||
shellEnv?: NodeJS.ProcessEnv | null;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
connectionMode?: 'strict' | 'augment';
|
||||
allowStoredApiKeyDecryption?: boolean;
|
||||
}
|
||||
|
||||
export interface ProviderAwareCliEnvResult {
|
||||
|
|
@ -28,6 +29,10 @@ export async function buildProviderAwareCliEnv(
|
|||
options: ProviderAwareCliEnvOptions = {}
|
||||
): Promise<ProviderAwareCliEnvResult> {
|
||||
const connectionMode = options.connectionMode ?? 'strict';
|
||||
const storedApiKeyAccessArgs =
|
||||
options.allowStoredApiKeyDecryption === undefined
|
||||
? []
|
||||
: [{ allowStoredApiKeyDecryption: options.allowStoredApiKeyDecryption }];
|
||||
const shellEnv = options.shellEnv ?? getCachedShellEnv() ?? {};
|
||||
const { env, resolvedProviderId } = buildRuntimeBaseEnv({
|
||||
binaryPath: options.binaryPath,
|
||||
|
|
@ -36,7 +41,7 @@ export async function buildProviderAwareCliEnv(
|
|||
shellEnv,
|
||||
env: options.env,
|
||||
});
|
||||
const appManagedOpenCodeBinary = resolveAppManagedOpenCodeRuntimeBinaryPath();
|
||||
const appManagedOpenCodeBinary = await resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath();
|
||||
if (
|
||||
appManagedOpenCodeBinary &&
|
||||
!env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH &&
|
||||
|
|
@ -53,7 +58,8 @@ export async function buildProviderAwareCliEnv(
|
|||
await providerConnectionService.augmentConfiguredConnectionEnv(
|
||||
env,
|
||||
resolvedProviderId,
|
||||
options.providerBackendId
|
||||
options.providerBackendId,
|
||||
...storedApiKeyAccessArgs
|
||||
);
|
||||
return {
|
||||
env,
|
||||
|
|
@ -65,7 +71,8 @@ export async function buildProviderAwareCliEnv(
|
|||
await providerConnectionService.applyConfiguredConnectionEnv(
|
||||
env,
|
||||
resolvedProviderId,
|
||||
options.providerBackendId
|
||||
options.providerBackendId,
|
||||
...storedApiKeyAccessArgs
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
@ -87,7 +94,10 @@ export async function buildProviderAwareCliEnv(
|
|||
}
|
||||
|
||||
if (connectionMode === 'augment') {
|
||||
await providerConnectionService.augmentAllConfiguredConnectionEnv(env);
|
||||
await providerConnectionService.augmentAllConfiguredConnectionEnv(
|
||||
env,
|
||||
...storedApiKeyAccessArgs
|
||||
);
|
||||
return {
|
||||
env,
|
||||
connectionIssues: {},
|
||||
|
|
@ -95,7 +105,7 @@ export async function buildProviderAwareCliEnv(
|
|||
};
|
||||
}
|
||||
|
||||
await providerConnectionService.applyAllConfiguredConnectionEnv(env);
|
||||
await providerConnectionService.applyAllConfiguredConnectionEnv(env, ...storedApiKeyAccessArgs);
|
||||
return {
|
||||
env,
|
||||
connectionIssues: await providerConnectionService.getConfiguredConnectionIssues(env),
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export interface TeamLaunchFailureArtifactPackResult {
|
|||
}
|
||||
|
||||
export type LaunchFailureArtifactClassificationCode =
|
||||
| 'workspace_trust_required'
|
||||
| 'transport_rejected'
|
||||
| 'stdin_missing'
|
||||
| 'provider_quota'
|
||||
|
|
@ -215,6 +216,13 @@ function firstEvidence(parts: readonly string[], pattern: RegExp): string[] {
|
|||
return evidence;
|
||||
}
|
||||
|
||||
const WORKSPACE_TRUST_FAILURE_PATTERN =
|
||||
/workspace trust is not accepted|cannot start in headless process runtime because workspace trust|open that workspace once interactively and accept trust/i;
|
||||
|
||||
export function isWorkspaceTrustLaunchFailureText(value: string): boolean {
|
||||
return WORKSPACE_TRUST_FAILURE_PATTERN.test(value);
|
||||
}
|
||||
|
||||
export function classifyLaunchFailureArtifact(
|
||||
input: TeamLaunchFailureArtifactPackInput
|
||||
): LaunchFailureArtifactClassification {
|
||||
|
|
@ -225,6 +233,11 @@ export function classifyLaunchFailureArtifact(
|
|||
confidence: number;
|
||||
pattern: RegExp;
|
||||
}[] = [
|
||||
{
|
||||
code: 'workspace_trust_required',
|
||||
confidence: 0.96,
|
||||
pattern: WORKSPACE_TRUST_FAILURE_PATTERN,
|
||||
},
|
||||
{
|
||||
code: 'transport_rejected',
|
||||
confidence: 0.95,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { createLogger } from '@shared/utils/logger';
|
||||
import { getTeamsBasePath } from '@main/utils/pathDecoder';
|
||||
import { createLogger } from '@shared/utils/logger';
|
||||
import { watch } from 'chokidar';
|
||||
import { createHash } from 'crypto';
|
||||
import * as fs from 'fs/promises';
|
||||
|
|
@ -14,7 +14,6 @@ import {
|
|||
BOARD_TASK_CHANGES_DIRNAME,
|
||||
BOARD_TASK_LOG_FRESHNESS_DIRNAME,
|
||||
BOARD_TASK_LOG_FRESHNESS_FILE_SUFFIX,
|
||||
TEAM_TASK_LOG_FRESHNESS_DIRNAME,
|
||||
classifyLogSourceWatcherEvent,
|
||||
getRelativeLogSourceParts,
|
||||
isAgentTranscriptFileName,
|
||||
|
|
@ -22,6 +21,7 @@ import {
|
|||
MAX_PENDING_UNKNOWN_ROOT_SESSIONS,
|
||||
normalizeLogSourceSessionId,
|
||||
PENDING_UNKNOWN_ROOT_SESSION_TTL_MS,
|
||||
TEAM_TASK_LOG_FRESHNESS_DIRNAME,
|
||||
} from './teamLogSourceWatchScope';
|
||||
|
||||
import type { TeamLogSourceLiveContext, TeamMemberLogsFinder } from './TeamMemberLogsFinder';
|
||||
|
|
|
|||
|
|
@ -101,6 +101,11 @@ interface ProjectSessionDiscovery {
|
|||
knownMembers: Set<string>;
|
||||
}
|
||||
|
||||
interface ProjectSessionDiscoveryOptions {
|
||||
forceRefresh?: boolean;
|
||||
includeTeamSubagentSessionDiscovery?: boolean;
|
||||
}
|
||||
|
||||
interface TaskMentionIndex {
|
||||
exactTaskIds: Set<string>;
|
||||
lowerTaskIds: Set<string>;
|
||||
|
|
@ -136,6 +141,7 @@ type FindRecentMemberLogFileRefsOptions =
|
|||
| {
|
||||
mtimeSinceMs?: number | null;
|
||||
forceRefresh?: boolean;
|
||||
includeTeamSubagentSessionDiscovery?: boolean;
|
||||
};
|
||||
|
||||
export interface TeamLogSourceLiveContext {
|
||||
|
|
@ -193,6 +199,14 @@ function collectTaskFreshnessRootDirs(candidates: readonly unknown[]): string[]
|
|||
return roots;
|
||||
}
|
||||
|
||||
function buildProjectSessionDiscoveryCacheKey(
|
||||
teamName: string,
|
||||
options?: ProjectSessionDiscoveryOptions
|
||||
): string {
|
||||
const subagentMode = options?.includeTeamSubagentSessionDiscovery === false ? 'known' : 'full';
|
||||
return `${teamName}\0${subagentMode}`;
|
||||
}
|
||||
|
||||
export class TeamMemberLogsFinder {
|
||||
private readonly taskMentionIndexCache = new Map<string, TaskMentionIndex>();
|
||||
private readonly taskMentionIndexInFlight = new Map<string, Promise<TaskMentionIndex>>();
|
||||
|
|
@ -200,6 +214,10 @@ export class TeamMemberLogsFinder {
|
|||
string,
|
||||
SubagentAttribution | RootSessionAttribution | null
|
||||
>();
|
||||
private readonly attributionInFlight = new Map<
|
||||
string,
|
||||
Promise<SubagentAttribution | RootSessionAttribution | null>
|
||||
>();
|
||||
private readonly discoveryCache = new Map<
|
||||
string,
|
||||
{
|
||||
|
|
@ -209,7 +227,11 @@ export class TeamMemberLogsFinder {
|
|||
>();
|
||||
private readonly discoveryInFlight = new Map<
|
||||
string,
|
||||
{ generation: number; promise: Promise<ProjectSessionDiscovery | null> }
|
||||
{
|
||||
generation: number;
|
||||
promise: Promise<ProjectSessionDiscovery | null>;
|
||||
forceRefresh: boolean;
|
||||
}
|
||||
>();
|
||||
private readonly discoveryGenerationByTeam = new Map<string, number>();
|
||||
|
||||
|
|
@ -979,10 +1001,16 @@ export class TeamMemberLogsFinder {
|
|||
): Promise<MemberLogFileRef[]> {
|
||||
const parsedOptions =
|
||||
typeof options === 'number' || options === null
|
||||
? { mtimeSinceMs: options ?? null, forceRefresh: false }
|
||||
? {
|
||||
mtimeSinceMs: options ?? null,
|
||||
forceRefresh: false,
|
||||
includeTeamSubagentSessionDiscovery: true,
|
||||
}
|
||||
: {
|
||||
mtimeSinceMs: options?.mtimeSinceMs ?? null,
|
||||
forceRefresh: options?.forceRefresh === true,
|
||||
includeTeamSubagentSessionDiscovery:
|
||||
options?.includeTeamSubagentSessionDiscovery !== false,
|
||||
};
|
||||
const requestedMembersByKey = new Map<string, string>();
|
||||
for (const memberName of memberNames) {
|
||||
|
|
@ -1001,6 +1029,7 @@ export class TeamMemberLogsFinder {
|
|||
|
||||
const discovery = await this.discoverProjectSessions(teamName, {
|
||||
forceRefresh: parsedOptions.forceRefresh,
|
||||
includeTeamSubagentSessionDiscovery: parsedOptions.includeTeamSubagentSessionDiscovery,
|
||||
});
|
||||
if (!discovery) {
|
||||
return [];
|
||||
|
|
@ -1147,41 +1176,50 @@ export class TeamMemberLogsFinder {
|
|||
|
||||
private async discoverProjectSessions(
|
||||
teamName: string,
|
||||
options?: { forceRefresh?: boolean }
|
||||
options?: ProjectSessionDiscoveryOptions
|
||||
): Promise<ProjectSessionDiscovery | null> {
|
||||
let generation = this.discoveryGenerationByTeam.get(teamName) ?? 0;
|
||||
const cacheKey = buildProjectSessionDiscoveryCacheKey(teamName, options);
|
||||
let generation = this.discoveryGenerationByTeam.get(cacheKey) ?? 0;
|
||||
if (options?.forceRefresh) {
|
||||
const inFlight = this.discoveryInFlight.get(cacheKey);
|
||||
if (inFlight?.forceRefresh === true) {
|
||||
return inFlight.promise;
|
||||
}
|
||||
generation += 1;
|
||||
this.discoveryGenerationByTeam.set(teamName, generation);
|
||||
this.discoveryCache.delete(teamName);
|
||||
this.discoveryInFlight.delete(teamName);
|
||||
this.discoveryGenerationByTeam.set(cacheKey, generation);
|
||||
this.discoveryCache.delete(cacheKey);
|
||||
} else {
|
||||
// Check discovery cache — avoids re-reading config/dirs within rapid successive calls
|
||||
const cached = this.discoveryCache.get(teamName);
|
||||
const cached = this.discoveryCache.get(cacheKey);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.result;
|
||||
}
|
||||
const inFlight = this.discoveryInFlight.get(teamName);
|
||||
const inFlight = this.discoveryInFlight.get(cacheKey);
|
||||
if (inFlight) {
|
||||
return inFlight.promise;
|
||||
}
|
||||
}
|
||||
|
||||
const promise = this.loadProjectSessionDiscovery(teamName, options, generation).finally(() => {
|
||||
const current = this.discoveryInFlight.get(teamName);
|
||||
const current = this.discoveryInFlight.get(cacheKey);
|
||||
if (current?.promise === promise) {
|
||||
this.discoveryInFlight.delete(teamName);
|
||||
this.discoveryInFlight.delete(cacheKey);
|
||||
}
|
||||
});
|
||||
this.discoveryInFlight.set(teamName, { generation, promise });
|
||||
this.discoveryInFlight.set(cacheKey, {
|
||||
generation,
|
||||
promise,
|
||||
forceRefresh: options?.forceRefresh === true,
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
private async loadProjectSessionDiscovery(
|
||||
teamName: string,
|
||||
options: { forceRefresh?: boolean } | undefined,
|
||||
options: ProjectSessionDiscoveryOptions | undefined,
|
||||
generation: number
|
||||
): Promise<ProjectSessionDiscovery | null> {
|
||||
const cacheKey = buildProjectSessionDiscoveryCacheKey(teamName, options);
|
||||
const context = await this.projectResolver.getContext(teamName, options);
|
||||
if (!context) {
|
||||
logger.debug(`No transcript context for team "${teamName}"`);
|
||||
|
|
@ -1214,8 +1252,8 @@ export class TeamMemberLogsFinder {
|
|||
}
|
||||
|
||||
const discovery = { projectDir, projectId, config, sessionIds, knownMembers };
|
||||
if ((this.discoveryGenerationByTeam.get(teamName) ?? 0) === generation) {
|
||||
this.discoveryCache.set(teamName, {
|
||||
if ((this.discoveryGenerationByTeam.get(cacheKey) ?? 0) === generation) {
|
||||
this.discoveryCache.set(cacheKey, {
|
||||
result: discovery,
|
||||
expiresAt: Date.now() + DISCOVERY_CACHE_TTL,
|
||||
});
|
||||
|
|
@ -1585,7 +1623,15 @@ export class TeamMemberLogsFinder {
|
|||
if (this.attributionCache.has(cacheKey)) {
|
||||
return this.attributionCache.get(cacheKey) ?? null;
|
||||
}
|
||||
const attribution = await this.attributeSubagent(filePath, knownMembers);
|
||||
const existing = this.attributionInFlight.get(cacheKey);
|
||||
if (existing) {
|
||||
return (await existing) as SubagentAttribution | null;
|
||||
}
|
||||
const promise = this.attributeSubagent(filePath, knownMembers).finally(() => {
|
||||
this.attributionInFlight.delete(cacheKey);
|
||||
});
|
||||
this.attributionInFlight.set(cacheKey, promise);
|
||||
const attribution = await promise;
|
||||
this.attributionCache.set(cacheKey, attribution);
|
||||
if (this.attributionCache.size > ATTRIBUTION_CACHE_MAX) {
|
||||
const oldestKey = this.attributionCache.keys().next().value;
|
||||
|
|
@ -1604,7 +1650,15 @@ export class TeamMemberLogsFinder {
|
|||
if (this.attributionCache.has(cacheKey)) {
|
||||
return (this.attributionCache.get(cacheKey) as RootSessionAttribution | null) ?? null;
|
||||
}
|
||||
const attribution = await this.attributeMemberSession(filePath, teamName, knownMembers);
|
||||
const existing = this.attributionInFlight.get(cacheKey);
|
||||
if (existing) {
|
||||
return (await existing) as RootSessionAttribution | null;
|
||||
}
|
||||
const promise = this.attributeMemberSession(filePath, teamName, knownMembers).finally(() => {
|
||||
this.attributionInFlight.delete(cacheKey);
|
||||
});
|
||||
this.attributionInFlight.set(cacheKey, promise);
|
||||
const attribution = await promise;
|
||||
this.attributionCache.set(cacheKey, attribution);
|
||||
if (this.attributionCache.size > ATTRIBUTION_CACHE_MAX) {
|
||||
const oldestKey = this.attributionCache.keys().next().value;
|
||||
|
|
|
|||
|
|
@ -298,7 +298,10 @@ import {
|
|||
import { TeamConfigReader } from './TeamConfigReader';
|
||||
import { TeamInboxReader } from './TeamInboxReader';
|
||||
import { TeamInboxWriter } from './TeamInboxWriter';
|
||||
import { writeTeamLaunchFailureArtifactPack } from './TeamLaunchFailureArtifactPack';
|
||||
import {
|
||||
isWorkspaceTrustLaunchFailureText,
|
||||
writeTeamLaunchFailureArtifactPack,
|
||||
} from './TeamLaunchFailureArtifactPack';
|
||||
import {
|
||||
createPersistedLaunchSnapshot,
|
||||
deriveTeamLaunchAggregateState,
|
||||
|
|
@ -924,6 +927,12 @@ function classifyDeterministicBootstrapFailure(reason: string): {
|
|||
} {
|
||||
const normalizedReason = reason.trim();
|
||||
const lower = normalizedReason.toLowerCase();
|
||||
if (isWorkspaceTrustLaunchFailureText(normalizedReason)) {
|
||||
return {
|
||||
title: 'Workspace trust required',
|
||||
normalizedReason,
|
||||
};
|
||||
}
|
||||
if (lower.includes('disabled by kill switch')) {
|
||||
return {
|
||||
title: 'Deterministic bootstrap disabled',
|
||||
|
|
@ -1860,6 +1869,8 @@ interface ProvisioningRun {
|
|||
lastMemberSpawnAuditConfigReadWarningAt: number;
|
||||
/** Per-member warning throttle for repeated "missing from config" logs. */
|
||||
lastMemberSpawnAuditMissingWarningAt: Map<string, number>;
|
||||
/** Prevents duplicate Team Launched notifications for the same live run. */
|
||||
teamLaunchedNotificationFired?: boolean;
|
||||
}
|
||||
|
||||
const PROVISIONING_TRACE_STORAGE_LIMIT = 500;
|
||||
|
|
@ -11825,12 +11836,7 @@ export class TeamProvisioningService {
|
|||
);
|
||||
this.invalidateRuntimeSnapshotCaches(input.teamName);
|
||||
if (trackedUpdate.changed) {
|
||||
this.teamChangeEmitter?.({
|
||||
type: 'member-spawn',
|
||||
teamName: input.teamName,
|
||||
runId: input.runId,
|
||||
detail: input.memberName,
|
||||
});
|
||||
this.emitMemberSpawnChange(trackedUpdate.run, input.memberName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -24788,7 +24794,7 @@ export class TeamProvisioningService {
|
|||
private emitMemberSpawnChange(
|
||||
run: Pick<ProvisioningRun, 'teamName' | 'runId'>,
|
||||
memberName: string
|
||||
) {
|
||||
): void {
|
||||
this.invalidateMemberSpawnStatusesCache(run.teamName);
|
||||
this.teamChangeEmitter?.({
|
||||
type: 'member-spawn',
|
||||
|
|
@ -24796,6 +24802,39 @@ export class TeamProvisioningService {
|
|||
runId: run.runId,
|
||||
detail: memberName,
|
||||
});
|
||||
const trackedRun = this.runs.get(run.runId);
|
||||
if (trackedRun?.teamName === run.teamName) {
|
||||
void this.maybeFireTeamLaunchedNotificationWhenAllMembersJoined(trackedRun);
|
||||
}
|
||||
}
|
||||
|
||||
private async maybeFireTeamLaunchedNotificationWhenAllMembersJoined(
|
||||
run: ProvisioningRun
|
||||
): Promise<void> {
|
||||
if (
|
||||
!run.isLaunch ||
|
||||
run.teamLaunchedNotificationFired ||
|
||||
run.processKilled ||
|
||||
run.cancelRequested ||
|
||||
!this.isProvisioningRunPromotedToAlive(run) ||
|
||||
!this.areAllExpectedLaunchMembersConfirmed(run)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fireTeamLaunchedNotification(run);
|
||||
}
|
||||
|
||||
private areAllExpectedLaunchMembersConfirmed(run: ProvisioningRun): boolean {
|
||||
const expectedMembers = run.expectedMembers ?? [];
|
||||
if (expectedMembers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return expectedMembers.every((memberName) => {
|
||||
const member = run.memberSpawnStatuses.get(memberName);
|
||||
return member?.launchState === 'confirmed_alive' || member?.bootstrapConfirmed === true;
|
||||
});
|
||||
}
|
||||
|
||||
private async publishMixedSecondaryLaneStatusChange(
|
||||
|
|
@ -30499,12 +30538,21 @@ export class TeamProvisioningService {
|
|||
* Uses the existing addTeamNotification() pipeline.
|
||||
*/
|
||||
private async fireTeamLaunchedNotification(run: ProvisioningRun): Promise<void> {
|
||||
if (run.teamLaunchedNotificationFired) {
|
||||
return;
|
||||
}
|
||||
run.teamLaunchedNotificationFired = true;
|
||||
|
||||
try {
|
||||
const config = ConfigManager.getInstance().getConfig();
|
||||
const suppressToast = !config.notifications.notifyOnTeamLaunched;
|
||||
const displayName = run.request.displayName || run.teamName;
|
||||
const joinedCount = run.expectedMembers?.length ?? 0;
|
||||
const allJoined = joinedCount > 0 && this.areAllExpectedLaunchMembersConfirmed(run);
|
||||
const body = run.isLaunch
|
||||
? `Team "${displayName}" has been launched and is ready for tasks.`
|
||||
? allJoined
|
||||
? `Team "${displayName}" has been launched - all ${joinedCount} teammates joined and are ready for tasks.`
|
||||
: `Team "${displayName}" has been launched and is ready for tasks.`
|
||||
: `Team "${displayName}" has been provisioned and is ready for tasks.`;
|
||||
|
||||
await NotificationManager.getInstance().addTeamNotification({
|
||||
|
|
@ -30520,6 +30568,7 @@ export class TeamProvisioningService {
|
|||
suppressToast,
|
||||
});
|
||||
} catch (error) {
|
||||
run.teamLaunchedNotificationFired = false;
|
||||
logger.warn(
|
||||
`[${run.teamName}] Failed to fire team_launched notification: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const logger = createLogger('Service:TeamTranscriptProjectResolver');
|
|||
const SESSION_DISCOVERY_CACHE_TTL = 30_000;
|
||||
const TEAM_AFFINITY_SCAN_LINES = 40;
|
||||
const ROOT_DISCOVERY_CONCURRENCY = 12;
|
||||
const FAST_CONTEXT_ROOT_DISCOVERY_MTIME_GRACE_MS = 24 * 60 * 60_000;
|
||||
|
||||
type ProjectEvidenceSource =
|
||||
| 'projectPath'
|
||||
|
|
@ -51,6 +52,11 @@ interface TeamTranscriptProjectConfigReader {
|
|||
getConfigSnapshot?: (teamName: string) => Promise<TeamConfig | null>;
|
||||
}
|
||||
|
||||
interface TeamTranscriptProjectContextOptions {
|
||||
forceRefresh?: boolean;
|
||||
includeTeamSubagentSessionDiscovery?: boolean;
|
||||
}
|
||||
|
||||
type ScannedSessionProjectMatch = Omit<SessionProjectMatch, 'projectPath'> & {
|
||||
projectPath?: string;
|
||||
};
|
||||
|
|
@ -72,6 +78,45 @@ function isSessionDirectoryName(name: string): boolean {
|
|||
return name !== 'memory' && !name.startsWith('.');
|
||||
}
|
||||
|
||||
function buildContextCacheKey(
|
||||
teamName: string,
|
||||
options?: TeamTranscriptProjectContextOptions
|
||||
): string {
|
||||
const subagentMode = options?.includeTeamSubagentSessionDiscovery === false ? 'known' : 'full';
|
||||
return `${teamName}\0${subagentMode}`;
|
||||
}
|
||||
|
||||
function parseTimestampMs(value: unknown): number | null {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string' && value.trim().length > 0) {
|
||||
const parsed = Date.parse(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function teamLifecycleMtimeCutoffMs(config: TeamConfig): number | null {
|
||||
const timestamps: number[] = [];
|
||||
const createdAt = parseTimestampMs((config as { createdAt?: unknown }).createdAt);
|
||||
if (createdAt !== null) {
|
||||
timestamps.push(createdAt);
|
||||
}
|
||||
|
||||
for (const member of config.members ?? []) {
|
||||
const joinedAt = parseTimestampMs((member as { joinedAt?: unknown }).joinedAt);
|
||||
if (joinedAt !== null) {
|
||||
timestamps.push(joinedAt);
|
||||
}
|
||||
}
|
||||
|
||||
if (timestamps.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.max(0, Math.min(...timestamps) - FAST_CONTEXT_ROOT_DISCOVERY_MTIME_GRACE_MS);
|
||||
}
|
||||
|
||||
function normalizeProjectPathCandidate(value: unknown): string | null {
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
|
|
@ -207,12 +252,21 @@ export class TeamTranscriptProjectResolver {
|
|||
: this.configReader.getConfig(teamName);
|
||||
}
|
||||
|
||||
private deleteContextCacheForTeam(teamName: string): void {
|
||||
this.contextCache.delete(teamName);
|
||||
for (const key of this.contextCache.keys()) {
|
||||
if (key === teamName || key.startsWith(`${teamName}\0`)) {
|
||||
this.contextCache.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getLiveBaseContext(
|
||||
teamName: string,
|
||||
options?: { forceRefresh?: boolean; extraProjectPathCandidates?: readonly unknown[] }
|
||||
): Promise<TeamTranscriptProjectLiveBaseContext | null> {
|
||||
if (options?.forceRefresh) {
|
||||
this.contextCache.delete(teamName);
|
||||
this.deleteContextCacheForTeam(teamName);
|
||||
}
|
||||
|
||||
const config = await this.readConfigForObservation(teamName);
|
||||
|
|
@ -244,13 +298,14 @@ export class TeamTranscriptProjectResolver {
|
|||
|
||||
async getContext(
|
||||
teamName: string,
|
||||
options?: { forceRefresh?: boolean }
|
||||
options?: TeamTranscriptProjectContextOptions
|
||||
): Promise<TeamTranscriptProjectContext | null> {
|
||||
const cacheKey = buildContextCacheKey(teamName, options);
|
||||
if (options?.forceRefresh) {
|
||||
this.contextCache.delete(teamName);
|
||||
this.deleteContextCacheForTeam(teamName);
|
||||
}
|
||||
|
||||
const cached = this.contextCache.get(teamName);
|
||||
const cached = this.contextCache.get(cacheKey);
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.value;
|
||||
}
|
||||
|
|
@ -282,7 +337,8 @@ export class TeamTranscriptProjectResolver {
|
|||
const sessionIds = await this.discoverSessionIds(
|
||||
teamName,
|
||||
resolution.projectDir,
|
||||
resolvedConfig
|
||||
resolvedConfig,
|
||||
options
|
||||
);
|
||||
const value = {
|
||||
projectDir: resolution.projectDir,
|
||||
|
|
@ -290,7 +346,7 @@ export class TeamTranscriptProjectResolver {
|
|||
config: resolvedConfig,
|
||||
sessionIds,
|
||||
};
|
||||
this.contextCache.set(teamName, {
|
||||
this.contextCache.set(cacheKey, {
|
||||
value,
|
||||
expiresAt: Date.now() + SESSION_DISCOVERY_CACHE_TTL,
|
||||
});
|
||||
|
|
@ -741,12 +797,20 @@ export class TeamTranscriptProjectResolver {
|
|||
private async discoverSessionIds(
|
||||
teamName: string,
|
||||
projectDir: string,
|
||||
config: TeamConfig
|
||||
config: TeamConfig,
|
||||
options?: TeamTranscriptProjectContextOptions
|
||||
): Promise<string[]> {
|
||||
const knownSessionIds = collectKnownSessionIds(config);
|
||||
const [teamRootSessionIds, sessionDirIds] = await Promise.all([
|
||||
this.listTeamRootSessionIds(projectDir, teamName),
|
||||
this.listSessionDirIds(projectDir),
|
||||
const includeTeamSubagentSessionDiscovery =
|
||||
options?.includeTeamSubagentSessionDiscovery !== false;
|
||||
const rootMtimeSinceMs = includeTeamSubagentSessionDiscovery
|
||||
? null
|
||||
: teamLifecycleMtimeCutoffMs(config);
|
||||
const [teamRootSessionIds, teamSubagentSessionIds] = await Promise.all([
|
||||
this.listTeamRootSessionIds(projectDir, teamName, rootMtimeSinceMs),
|
||||
includeTeamSubagentSessionDiscovery
|
||||
? this.listTeamSubagentSessionIds(projectDir, teamName)
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
const orderedSessionIds: string[] = [];
|
||||
|
|
@ -762,7 +826,7 @@ export class TeamTranscriptProjectResolver {
|
|||
for (const sessionId of knownSessionIds) {
|
||||
push(sessionId);
|
||||
}
|
||||
for (const sessionId of [...teamRootSessionIds, ...sessionDirIds].sort((left, right) =>
|
||||
for (const sessionId of [...teamRootSessionIds, ...teamSubagentSessionIds].sort((left, right) =>
|
||||
left.localeCompare(right)
|
||||
)) {
|
||||
push(sessionId);
|
||||
|
|
@ -816,21 +880,69 @@ export class TeamTranscriptProjectResolver {
|
|||
}
|
||||
}
|
||||
|
||||
private async listSessionDirIds(projectDir: string): Promise<string[]> {
|
||||
private async listTeamSubagentSessionIds(
|
||||
projectDir: string,
|
||||
teamName: string
|
||||
): Promise<string[]> {
|
||||
const dirEntries = await this.readProjectDirEntries(projectDir);
|
||||
if (!dirEntries) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return dirEntries
|
||||
.filter((entry) => entry.isDirectory() && isSessionDirectoryName(entry.name))
|
||||
.map((entry) => entry.name);
|
||||
const sessionDirEntries = dirEntries.filter(
|
||||
(entry) => entry.isDirectory() && isSessionDirectoryName(entry.name)
|
||||
);
|
||||
const discovered = new Set<string>();
|
||||
let nextIndex = 0;
|
||||
|
||||
const scanNextSessionDir = async (): Promise<void> => {
|
||||
while (nextIndex < sessionDirEntries.length) {
|
||||
const entry = sessionDirEntries[nextIndex++];
|
||||
const subagentsDir = path.join(projectDir, entry.name, 'subagents');
|
||||
let subagentEntries: Dirent[];
|
||||
try {
|
||||
subagentEntries = await fs.readdir(subagentsDir, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const subagentEntry of subagentEntries) {
|
||||
if (!subagentEntry.isFile()) {
|
||||
continue;
|
||||
}
|
||||
if (!subagentEntry.name.endsWith('.jsonl')) {
|
||||
continue;
|
||||
}
|
||||
if (!subagentEntry.name.startsWith('agent-')) {
|
||||
continue;
|
||||
}
|
||||
if (subagentEntry.name.startsWith('agent-acompact')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const filePath = path.join(subagentsDir, subagentEntry.name);
|
||||
if (await this.fileBelongsToTeam(filePath, teamName)) {
|
||||
discovered.add(entry.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.min(ROOT_DISCOVERY_CONCURRENCY, sessionDirEntries.length) }, () =>
|
||||
scanNextSessionDir()
|
||||
)
|
||||
);
|
||||
|
||||
return [...discovered];
|
||||
}
|
||||
|
||||
private async collectRootJsonlSessionIds(
|
||||
rootJsonlEntries: Dirent[],
|
||||
projectDir: string,
|
||||
teamName: string
|
||||
teamName: string,
|
||||
mtimeSinceMs?: number | null
|
||||
): Promise<string[]> {
|
||||
const discovered = new Set<string>();
|
||||
let nextIndex = 0;
|
||||
|
|
@ -839,6 +951,16 @@ export class TeamTranscriptProjectResolver {
|
|||
while (nextIndex < rootJsonlEntries.length) {
|
||||
const entry = rootJsonlEntries[nextIndex++];
|
||||
const filePath = path.join(projectDir, entry.name);
|
||||
if (mtimeSinceMs != null) {
|
||||
try {
|
||||
const stat = await fs.stat(filePath);
|
||||
if (!stat.isFile() || stat.mtimeMs < mtimeSinceMs) {
|
||||
continue;
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!(await this.fileBelongsToTeam(filePath, teamName))) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -855,7 +977,11 @@ export class TeamTranscriptProjectResolver {
|
|||
return [...discovered];
|
||||
}
|
||||
|
||||
private async listTeamRootSessionIds(projectDir: string, teamName: string): Promise<string[]> {
|
||||
private async listTeamRootSessionIds(
|
||||
projectDir: string,
|
||||
teamName: string,
|
||||
mtimeSinceMs?: number | null
|
||||
): Promise<string[]> {
|
||||
const dirEntries = await this.readProjectDirEntries(projectDir);
|
||||
if (!dirEntries) {
|
||||
return [];
|
||||
|
|
@ -864,7 +990,7 @@ export class TeamTranscriptProjectResolver {
|
|||
const rootJsonlEntries = dirEntries.filter(
|
||||
(entry) => entry.isFile() && entry.name.endsWith('.jsonl')
|
||||
);
|
||||
return this.collectRootJsonlSessionIds(rootJsonlEntries, projectDir, teamName);
|
||||
return this.collectRootJsonlSessionIds(rootJsonlEntries, projectDir, teamName, mtimeSinceMs);
|
||||
}
|
||||
|
||||
private async fileBelongsToTeam(filePath: string, teamName: string): Promise<boolean> {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { randomUUID } from 'crypto';
|
||||
|
||||
import type { OpenCodeTeamRuntimeBridgePort } from '../../runtime/OpenCodeTeamRuntimeAdapter';
|
||||
import { stableHash } from './OpenCodeBridgeCommandContract';
|
||||
|
||||
import type { OpenCodeTeamRuntimeBridgePort } from '../../runtime/OpenCodeTeamRuntimeAdapter';
|
||||
import type {
|
||||
OpenCodeTeamLaunchReadiness,
|
||||
OpenCodeTeamLaunchReadinessState,
|
||||
|
|
@ -14,10 +15,10 @@ import type {
|
|||
OpenCodeBridgeFailureKind,
|
||||
OpenCodeBridgeResult,
|
||||
OpenCodeBridgeRuntimeSnapshot,
|
||||
OpenCodeCommandStatusCommandBody,
|
||||
OpenCodeCommandStatusCommandData,
|
||||
OpenCodeCleanupHostsCommandBody,
|
||||
OpenCodeCleanupHostsCommandData,
|
||||
OpenCodeCommandStatusCommandBody,
|
||||
OpenCodeCommandStatusCommandData,
|
||||
OpenCodeLaunchTeamCommandBody,
|
||||
OpenCodeLaunchTeamCommandData,
|
||||
OpenCodeObserveMessageDeliveryCommandBody,
|
||||
|
|
@ -79,10 +80,6 @@ const DEFAULT_CLEANUP_TIMEOUT_MS = 10_000;
|
|||
const DEFAULT_BACKFILL_TIMEOUT_MS = 45_000;
|
||||
const DEFAULT_COMMAND_STATUS_TIMEOUT_MS = 5_000;
|
||||
|
||||
function isCommandStatusRecoveryEnabled(): boolean {
|
||||
return process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY === '1';
|
||||
}
|
||||
|
||||
function buildSendPayloadHash(input: OpenCodeSendMessageCommandBody): string {
|
||||
const { payloadHash: _payloadHash, ...hashable } = input;
|
||||
return stableHash(hashable);
|
||||
|
|
@ -246,7 +243,7 @@ export class OpenCodeReadinessBridge implements OpenCodeTeamRuntimeBridgePort {
|
|||
if (result.ok) {
|
||||
return result.data;
|
||||
}
|
||||
if (result.error.kind === 'timeout' && isCommandStatusRecoveryEnabled()) {
|
||||
if (result.error.kind === 'timeout') {
|
||||
const recovered = await this.recoverTimedOutSendMessage({
|
||||
originalRequestId: commandRequestId,
|
||||
body,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { classifyRuntimeDiagnostic } from '../../runtime/RuntimeDiagnosticClassifier';
|
||||
|
||||
import {
|
||||
isActionRequiredOpenCodeRuntimeDeliveryReason,
|
||||
selectOpenCodeRuntimeDeliveryReason,
|
||||
} from './OpenCodeRuntimeDeliveryDiagnostics';
|
||||
import { classifyRuntimeDiagnostic } from '../../runtime/RuntimeDiagnosticClassifier';
|
||||
|
||||
import type { OpenCodePromptDeliveryLedgerRecord } from './OpenCodePromptDeliveryLedger';
|
||||
import type {
|
||||
|
|
|
|||
|
|
@ -621,6 +621,13 @@ function getOpenCodeInstallLabel(status: OpenCodeRuntimeStatus | null): string {
|
|||
return 'Install';
|
||||
}
|
||||
|
||||
const OPENCODE_PROVIDER_FREE_BADGE_TITLE =
|
||||
'OpenCode includes free model options such as Big Pickle when available in your setup. OpenRouter through OpenCode can also expose free models, but not every OpenCode/OpenRouter model is free. Availability and limits may change.';
|
||||
|
||||
function shouldShowOpenCodeProviderFreeBadge(provider: CliProviderStatus): boolean {
|
||||
return provider.providerId === 'opencode';
|
||||
}
|
||||
|
||||
const InstalledBanner = ({
|
||||
cliStatus,
|
||||
sourceProviderMap,
|
||||
|
|
@ -847,6 +854,14 @@ const InstalledBanner = ({
|
|||
? getProviderLabel(provider.providerId)
|
||||
: provider.displayName}
|
||||
</span>
|
||||
{shouldShowOpenCodeProviderFreeBadge(provider) ? (
|
||||
<span
|
||||
className="rounded bg-[rgba(34,197,94,0.14)] px-1.5 py-px text-[9px] font-medium uppercase tracking-[0.06em] text-[rgb(74,222,128)]"
|
||||
title={OPENCODE_PROVIDER_FREE_BADGE_TITLE}
|
||||
>
|
||||
Free models
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs"
|
||||
|
|
@ -1039,6 +1054,7 @@ const InstalledBanner = ({
|
|||
modelAvailability={provider.modelAvailability}
|
||||
providerStatus={provider}
|
||||
collapseAfter={15}
|
||||
maxCollapsedRows={provider.providerId === 'opencode' ? 2 : undefined}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -270,11 +270,6 @@ export const ExtensionStoreView = (): React.JSX.Element => {
|
|||
void mcpFetchInstalled(projectPath ?? undefined);
|
||||
}, [mcpFetchInstalled, projectPath]);
|
||||
|
||||
// Fetch API keys on mount
|
||||
useEffect(() => {
|
||||
void fetchApiKeys();
|
||||
}, [fetchApiKeys]);
|
||||
|
||||
// Fetch Skills catalog on mount / project change
|
||||
useEffect(() => {
|
||||
void fetchSkillsCatalog(projectPath ?? undefined);
|
||||
|
|
@ -287,7 +282,9 @@ export const ExtensionStoreView = (): React.JSX.Element => {
|
|||
bootstrapCliStatus,
|
||||
fetchCliStatus,
|
||||
});
|
||||
void fetchApiKeys();
|
||||
if (tabState.activeSubTab === 'api-keys') {
|
||||
void fetchApiKeys();
|
||||
}
|
||||
void fetchPluginCatalog(projectPath ?? undefined, true);
|
||||
void mcpBrowse(); // re-fetch first page
|
||||
void mcpFetchInstalled(projectPath ?? undefined);
|
||||
|
|
@ -302,6 +299,7 @@ export const ExtensionStoreView = (): React.JSX.Element => {
|
|||
mcpBrowse,
|
||||
mcpFetchInstalled,
|
||||
projectPath,
|
||||
tabState.activeSubTab,
|
||||
]);
|
||||
|
||||
const isRefreshing =
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export const ApiKeysPanel = ({
|
|||
apiKeysLoading,
|
||||
apiKeysError,
|
||||
storageStatus,
|
||||
fetchApiKeys,
|
||||
fetchStorageStatus,
|
||||
cliStatus,
|
||||
cliStatusLoading,
|
||||
|
|
@ -46,6 +47,7 @@ export const ApiKeysPanel = ({
|
|||
apiKeysLoading: s.apiKeysLoading,
|
||||
apiKeysError: s.apiKeysError,
|
||||
storageStatus: s.apiKeyStorageStatus,
|
||||
fetchApiKeys: s.fetchApiKeys,
|
||||
fetchStorageStatus: s.fetchApiKeyStorageStatus,
|
||||
cliStatus: s.cliStatus,
|
||||
cliStatusLoading: s.cliStatusLoading,
|
||||
|
|
@ -85,8 +87,9 @@ export const ApiKeysPanel = ({
|
|||
const [editingKey, setEditingKey] = useState<ApiKeyEntry | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchApiKeys();
|
||||
void fetchStorageStatus();
|
||||
}, [fetchStorageStatus]);
|
||||
}, [fetchApiKeys, fetchStorageStatus]);
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingKey(null);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
|
||||
import { cn } from '@renderer/lib/utils';
|
||||
import {
|
||||
|
|
@ -56,13 +56,43 @@ function getCatalogBadgeLabel(
|
|||
return catalogItem?.badgeLabel?.trim() || null;
|
||||
}
|
||||
|
||||
function normalizeBadgeText(value: string): string {
|
||||
return value.trim().replace(/\s+/g, ' ').toLowerCase();
|
||||
}
|
||||
|
||||
function shouldRenderCatalogBadge(modelLabel: string, catalogBadgeLabel: string | null): boolean {
|
||||
if (!catalogBadgeLabel) {
|
||||
return false;
|
||||
}
|
||||
return normalizeBadgeText(modelLabel) !== normalizeBadgeText(catalogBadgeLabel);
|
||||
}
|
||||
|
||||
function hasChildAfterRowLimit(container: HTMLElement, rowLimit: number): boolean {
|
||||
const rowTops: number[] = [];
|
||||
const children = Array.from(container.children) as HTMLElement[];
|
||||
|
||||
for (const child of children) {
|
||||
const top = child.offsetTop;
|
||||
let rowIndex = rowTops.findIndex((rowTop) => Math.abs(rowTop - top) <= 1);
|
||||
if (rowIndex < 0) {
|
||||
rowTops.push(top);
|
||||
rowIndex = rowTops.length - 1;
|
||||
}
|
||||
if (rowIndex >= rowLimit) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export const ProviderModelBadges = ({
|
||||
providerId,
|
||||
models,
|
||||
modelAvailability,
|
||||
providerStatus,
|
||||
collapseAfter,
|
||||
expandedMaxHeightPx = 200,
|
||||
maxCollapsedRows,
|
||||
}: {
|
||||
readonly providerId: CliProviderId;
|
||||
readonly models: string[];
|
||||
|
|
@ -72,16 +102,73 @@ export const ProviderModelBadges = ({
|
|||
'providerId' | 'authMethod' | 'backend' | 'modelCatalog'
|
||||
> | null;
|
||||
readonly collapseAfter?: number;
|
||||
readonly expandedMaxHeightPx?: number;
|
||||
readonly maxCollapsedRows?: number;
|
||||
}): React.JSX.Element => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [collapsedModelLimit, setCollapsedModelLimit] = useState<number | null>(null);
|
||||
const [measureTick, setMeasureTick] = useState(0);
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const visibleModels = getVisibleTeamProviderModels(providerId, models, providerStatus);
|
||||
const displayModelAvailability = providerId === 'opencode' ? undefined : modelAvailability;
|
||||
const shouldCollapse =
|
||||
typeof collapseAfter === 'number' && collapseAfter > 0 && visibleModels.length > collapseAfter;
|
||||
const collapsedBaseLimit = shouldCollapse ? collapseAfter : visibleModels.length;
|
||||
const collapsedLimit =
|
||||
shouldCollapse && !expanded
|
||||
? Math.max(0, Math.min(collapsedModelLimit ?? collapsedBaseLimit, collapsedBaseLimit))
|
||||
: visibleModels.length;
|
||||
const displayedModels =
|
||||
shouldCollapse && !expanded ? visibleModels.slice(0, collapseAfter) : visibleModels;
|
||||
const hiddenCount = shouldCollapse ? visibleModels.length - collapseAfter : 0;
|
||||
shouldCollapse && !expanded ? visibleModels.slice(0, collapsedLimit) : visibleModels;
|
||||
const hiddenCount = shouldCollapse ? visibleModels.length - displayedModels.length : 0;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setCollapsedModelLimit(null);
|
||||
}, [collapseAfter, maxCollapsedRows, models, providerStatus]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!shouldCollapse || expanded || !maxCollapsedRows || maxCollapsedRows < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = listRef.current;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasChildAfterRowLimit(container, maxCollapsedRows)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextLimit = Math.max(0, collapsedLimit - 1);
|
||||
if (nextLimit !== collapsedLimit) {
|
||||
setCollapsedModelLimit(nextLimit);
|
||||
}
|
||||
}, [collapsedLimit, expanded, maxCollapsedRows, measureTick, shouldCollapse]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!shouldCollapse || expanded || !maxCollapsedRows || typeof ResizeObserver === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = listRef.current;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lastWidth = container.clientWidth;
|
||||
const observer = new ResizeObserver((entries) => {
|
||||
const width = Math.round(entries[0]?.contentRect.width ?? container.clientWidth);
|
||||
if (width === lastWidth) {
|
||||
return;
|
||||
}
|
||||
lastWidth = width;
|
||||
setCollapsedModelLimit(null);
|
||||
setMeasureTick((value) => value + 1);
|
||||
});
|
||||
|
||||
observer.observe(container);
|
||||
return () => observer.disconnect();
|
||||
}, [expanded, maxCollapsedRows, shouldCollapse]);
|
||||
|
||||
const badgeClassName =
|
||||
'inline-flex items-center gap-1 rounded-md border px-1.5 py-px font-mono text-[10px] leading-4';
|
||||
|
|
@ -92,20 +179,18 @@ export const ProviderModelBadges = ({
|
|||
};
|
||||
const buttonClassName =
|
||||
'inline-flex items-center gap-1 rounded-full border border-[rgba(59,130,246,0.35)] bg-[rgba(59,130,246,0.12)] px-2 py-px text-[10px] font-medium leading-4 text-[rgb(147,197,253)] transition-colors hover:border-[rgba(59,130,246,0.55)] hover:bg-[rgba(59,130,246,0.18)] hover:text-[rgb(191,219,254)]';
|
||||
const listClassName = cn('flex flex-wrap gap-1.5', expanded && shouldCollapse ? 'pr-1' : null);
|
||||
const listStyle =
|
||||
expanded && shouldCollapse
|
||||
? ({ maxHeight: expandedMaxHeightPx, overflowY: 'auto' } as const)
|
||||
: undefined;
|
||||
const listClassName = cn('flex flex-wrap gap-1.5');
|
||||
|
||||
const renderModelBadge = (model: string, index: number): React.JSX.Element => {
|
||||
const availabilityStatus = getAvailabilityStatus(model, displayModelAvailability);
|
||||
const availabilityReason = getAvailabilityReason(model, displayModelAvailability);
|
||||
const availabilityChip = getAvailabilityChip(availabilityStatus);
|
||||
const modelLabel = formatModelBadgeLabel(providerId, model);
|
||||
const catalogBadgeLabel = getCatalogBadgeLabel(model, providerStatus);
|
||||
const showCatalogBadge = shouldRenderCatalogBadge(modelLabel, catalogBadgeLabel);
|
||||
const title = [
|
||||
availabilityReason ?? availabilityChip,
|
||||
catalogBadgeLabel === 'Free'
|
||||
showCatalogBadge && catalogBadgeLabel === 'Free'
|
||||
? 'Reported by OpenCode metadata. Availability and limits may change.'
|
||||
: null,
|
||||
]
|
||||
|
|
@ -119,8 +204,8 @@ export const ProviderModelBadges = ({
|
|||
style={badgeStyle}
|
||||
title={title || undefined}
|
||||
>
|
||||
<span>{formatModelBadgeLabel(providerId, model)}</span>
|
||||
{catalogBadgeLabel ? (
|
||||
<span>{modelLabel}</span>
|
||||
{showCatalogBadge ? (
|
||||
<span className="rounded bg-[rgba(34,197,94,0.14)] px-1 py-0 text-[9px] font-medium uppercase tracking-[0.06em] text-[rgb(74,222,128)]">
|
||||
{catalogBadgeLabel}
|
||||
</span>
|
||||
|
|
@ -149,7 +234,7 @@ export const ProviderModelBadges = ({
|
|||
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-1.5">
|
||||
<div className={listClassName} style={listStyle}>
|
||||
<div ref={listRef} className={listClassName}>
|
||||
{displayedModels.map(renderModelBadge)}
|
||||
{shouldCollapse && !expanded ? (
|
||||
<button type="button" className={buttonClassName} onClick={() => setExpanded(true)}>
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@ import type {
|
|||
TeamCreateRequest,
|
||||
TeamLaunchRequest,
|
||||
TeamProviderId,
|
||||
TeamSummary,
|
||||
TeamTaskWithKanban,
|
||||
} from '@shared/types';
|
||||
import type { EditorSelectionAction } from '@shared/types/editor';
|
||||
|
|
@ -174,6 +175,61 @@ interface CreateTaskDialogState {
|
|||
|
||||
const TEAM_PENDING_REPLY_REFRESH_DELAY_MS = 10_000;
|
||||
|
||||
function getSummaryKnownTeammateCount(summary: TeamSummary | undefined): number {
|
||||
if (!summary) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const normalizedLeadName = summary.leadName?.trim().toLowerCase();
|
||||
const rosterNames = new Set<string>();
|
||||
for (const member of summary.members ?? []) {
|
||||
const name = member.name?.trim();
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const normalizedName = name.toLowerCase();
|
||||
if (
|
||||
normalizedName === 'user' ||
|
||||
isLeadMember({ name }) ||
|
||||
(normalizedLeadName && normalizedName === normalizedLeadName)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
rosterNames.add(normalizedName);
|
||||
}
|
||||
|
||||
const launchNames = new Set<string>();
|
||||
for (const rawName of [...(summary.missingMembers ?? []), ...(summary.skippedMembers ?? [])]) {
|
||||
const name = rawName.trim();
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const normalizedName = name.toLowerCase();
|
||||
if (
|
||||
normalizedName === 'user' ||
|
||||
isLeadMember({ name }) ||
|
||||
(normalizedLeadName && normalizedName === normalizedLeadName)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
launchNames.add(normalizedName);
|
||||
}
|
||||
|
||||
const activeRosterCount = Math.max(summary.memberCount, rosterNames.size);
|
||||
if (activeRosterCount > 0) {
|
||||
return activeRosterCount;
|
||||
}
|
||||
|
||||
return Math.max(
|
||||
summary.expectedMemberCount ?? 0,
|
||||
launchNames.size,
|
||||
(summary.confirmedCount ?? 0) +
|
||||
(summary.pendingCount ?? 0) +
|
||||
(summary.failedCount ?? 0) +
|
||||
(summary.skippedCount ?? 0)
|
||||
);
|
||||
}
|
||||
|
||||
function areResolvedMembersEqual(
|
||||
prev: readonly ResolvedTeamMember[],
|
||||
next: readonly ResolvedTeamMember[]
|
||||
|
|
@ -1578,6 +1634,7 @@ export const TeamDetailView = memo(function TeamDetailView({
|
|||
selectReviewFile,
|
||||
pendingReviewRequest,
|
||||
setPendingReviewRequest,
|
||||
summaryKnownTeammateCount,
|
||||
} = useStore(
|
||||
useShallow((s) => ({
|
||||
projects: s.projects,
|
||||
|
|
@ -1612,6 +1669,9 @@ export const TeamDetailView = memo(function TeamDetailView({
|
|||
isTeamProvisioning: teamName ? isTeamProvisioningActive(s, teamName) : false,
|
||||
data: s.selectedTeamName === teamName ? s.selectedTeamData : null,
|
||||
members: selectResolvedMembersForTeamName(s, teamName),
|
||||
summaryKnownTeammateCount: teamName
|
||||
? getSummaryKnownTeammateCount(s.teamByName[teamName])
|
||||
: 0,
|
||||
loading: s.selectedTeamName === teamName ? s.selectedTeamLoading : false,
|
||||
error: s.selectedTeamName === teamName ? s.selectedTeamError : null,
|
||||
refreshTeamData: s.refreshTeamData,
|
||||
|
|
@ -1995,10 +2055,13 @@ export const TeamDetailView = memo(function TeamDetailView({
|
|||
return filterKanbanTasks(filteredTasks, kanbanSearchQuery);
|
||||
}, [filteredTasks, kanbanSearchQuery]);
|
||||
|
||||
const activeTeammateCount = useMemo(
|
||||
() => activeMembers.filter((m) => !isLeadMember(m)).length,
|
||||
[activeMembers]
|
||||
);
|
||||
const activeTeammateCount = useMemo(() => {
|
||||
const resolvedCount = activeMembers.filter((m) => !isLeadMember(m)).length;
|
||||
if (membersWithLiveBranches.some((m) => m.removedAt)) {
|
||||
return resolvedCount;
|
||||
}
|
||||
return resolvedCount > 0 ? resolvedCount : summaryKnownTeammateCount;
|
||||
}, [activeMembers, membersWithLiveBranches, summaryKnownTeammateCount]);
|
||||
const leadProviderId = useMemo<TeamProviderId | undefined>(() => {
|
||||
const activeLeadProviderId = activeMembers.find(isLeadMember)?.providerId;
|
||||
if (activeLeadProviderId) return activeLeadProviderId;
|
||||
|
|
@ -2814,6 +2877,7 @@ export const TeamDetailView = memo(function TeamDetailView({
|
|||
<TeamMemberListBridge
|
||||
teamName={teamName}
|
||||
members={membersWithLiveBranches}
|
||||
expectedTeammateCount={activeTeammateCount}
|
||||
memberTaskCounts={memberTaskCounts}
|
||||
taskMap={taskMap}
|
||||
pendingRepliesByMember={pendingRepliesByMember}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import type {
|
|||
interface MemberListProps {
|
||||
teamName?: string;
|
||||
members: ResolvedTeamMember[];
|
||||
expectedTeammateCount?: number;
|
||||
memberTaskCounts?: Map<string, TaskStatusCounts>;
|
||||
taskMap?: Map<string, TeamTaskWithKanban>;
|
||||
pendingRepliesByMember?: Record<string, number>;
|
||||
|
|
@ -312,6 +313,7 @@ function areMemberListPropsEqual(
|
|||
return (
|
||||
prev.teamName === next.teamName &&
|
||||
areResolvedMembersEquivalent(prev.members, next.members) &&
|
||||
prev.expectedTeammateCount === next.expectedTeammateCount &&
|
||||
areTaskStatusCountsMapsEquivalent(prev.memberTaskCounts, next.memberTaskCounts) &&
|
||||
areMemberTaskMapsEquivalent(prev.taskMap, next.taskMap) &&
|
||||
arePendingRepliesEquivalent(prev.pendingRepliesByMember, next.pendingRepliesByMember) &&
|
||||
|
|
@ -452,6 +454,7 @@ const MemberCardRow = memo(function MemberCardRow({
|
|||
export const MemberList = memo(function MemberList({
|
||||
teamName = '__unknown_team__',
|
||||
members,
|
||||
expectedTeammateCount,
|
||||
memberTaskCounts,
|
||||
taskMap,
|
||||
pendingRepliesByMember,
|
||||
|
|
@ -502,6 +505,10 @@ export const MemberList = memo(function MemberList({
|
|||
[members]
|
||||
);
|
||||
const removedMembers = useMemo(() => members.filter((m) => m.removedAt), [members]);
|
||||
const activeTeammateCount = useMemo(
|
||||
() => activeMembers.filter((member) => !isLeadMember(member)).length,
|
||||
[activeMembers]
|
||||
);
|
||||
const colorMap = useMemo(() => buildMemberColorMap(members), [members]);
|
||||
// Pre-compute reviewer->task map to avoid O(n*n) scan per member.
|
||||
const reviewTaskByMember = useMemo(() => {
|
||||
|
|
@ -644,10 +651,22 @@ export const MemberList = memo(function MemberList({
|
|||
[launchParams]
|
||||
);
|
||||
|
||||
if (members.length === 0) {
|
||||
const expectsTeammates = (expectedTeammateCount ?? 0) > 0;
|
||||
const hasOnlyLeadWhileTeammatesLoad =
|
||||
expectsTeammates && activeTeammateCount === 0 && removedMembers.length === 0;
|
||||
|
||||
if (members.length === 0 || hasOnlyLeadWhileTeammatesLoad) {
|
||||
if (expectsTeammates) {
|
||||
return (
|
||||
<div className="rounded-md border border-[var(--color-border)] p-4 text-sm text-[var(--color-text-muted)]">
|
||||
Team members are loading
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-[var(--color-border)] p-4 text-sm text-[var(--color-text-muted)]">
|
||||
Solo team — lead only
|
||||
Solo team - lead only
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,9 +110,9 @@ export function buildTeamChangeRequestPlan(
|
|||
}
|
||||
seenTaskIds.add(task.id);
|
||||
|
||||
const options = buildTaskChangeRequestOptions(task, { summaryOnly: true });
|
||||
const requestOptions = buildTaskChangeRequestOptions(task, { summaryOnly: true });
|
||||
const presence = task.changePresence ?? 'unknown';
|
||||
const canDisplay = canDisplayTaskChangesForOptions(options);
|
||||
const canDisplay = canDisplayTaskChangesForOptions(requestOptions);
|
||||
const shouldScanUnknown =
|
||||
presence === 'unknown' && (canDisplay || hasTaskChangeScanEvidence(task));
|
||||
if (
|
||||
|
|
@ -125,19 +125,19 @@ export function buildTeamChangeRequestPlan(
|
|||
}
|
||||
|
||||
if (presence === 'has_changes') {
|
||||
primary.push({ task, options, priority: 0 });
|
||||
primary.push({ task, options: requestOptions, priority: 0 });
|
||||
continue;
|
||||
}
|
||||
if (presence === 'needs_attention') {
|
||||
primary.push({ task, options, priority: 1 });
|
||||
primary.push({ task, options: requestOptions, priority: 1 });
|
||||
continue;
|
||||
}
|
||||
if (options.stateBucket === 'active' && options.status === 'in_progress') {
|
||||
active.push({ task, options, priority: 2 });
|
||||
if (requestOptions.stateBucket === 'active' && requestOptions.status === 'in_progress') {
|
||||
active.push({ task, options: requestOptions, priority: 2 });
|
||||
continue;
|
||||
}
|
||||
if (shouldScanUnknown) {
|
||||
unknown.push({ task, options, priority: 3 });
|
||||
unknown.push({ task, options: requestOptions, priority: 3 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,15 +166,15 @@ export function buildTeamChangeRequestPlan(
|
|||
const selected = [...requestPrimary, ...requestActive, ...unknownWindow].slice(0, maxRequests);
|
||||
const requestOptionsByTaskId = new Map<string, TaskChangeRequestOptions>();
|
||||
const requests = selected.map((candidate) => {
|
||||
const options = {
|
||||
const requestOptions = {
|
||||
...candidate.options,
|
||||
summaryOnly: true,
|
||||
forceFresh: forceFresh ? true : candidate.options.forceFresh,
|
||||
};
|
||||
requestOptionsByTaskId.set(candidate.task.id, options);
|
||||
requestOptionsByTaskId.set(candidate.task.id, requestOptions);
|
||||
return {
|
||||
taskId: candidate.task.id,
|
||||
options,
|
||||
options: requestOptions,
|
||||
};
|
||||
});
|
||||
const eligibleCount = primary.length + active.length + unknown.length;
|
||||
|
|
|
|||
|
|
@ -196,8 +196,7 @@ function isDocumentHidden(): boolean {
|
|||
|
||||
function isSilentCounterLoad(options: TeamChangesLoadOptions | null): boolean {
|
||||
return Boolean(
|
||||
options &&
|
||||
options.storeSummaries === false &&
|
||||
options?.storeSummaries === false &&
|
||||
options.reportError === false &&
|
||||
options.showSpinner !== true
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1983,6 +1983,165 @@ function buildConfigFallbackMemberSnapshots(snapshot: TeamViewSnapshot): TeamMem
|
|||
return fallbackMembers;
|
||||
}
|
||||
|
||||
function getActiveRawTeammateNameKeys(snapshot: TeamViewSnapshot | null | undefined): string[] {
|
||||
if (!snapshot) {
|
||||
return [];
|
||||
}
|
||||
const names = new Set<string>();
|
||||
for (const member of snapshot.members) {
|
||||
const name = member.name.trim();
|
||||
const key = name.toLowerCase();
|
||||
if (!name || key === 'user' || member.removedAt || isLeadMember(member)) {
|
||||
continue;
|
||||
}
|
||||
names.add(key);
|
||||
}
|
||||
return Array.from(names).sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function hasActiveRawTeammateRoster(snapshot: TeamViewSnapshot | null | undefined): boolean {
|
||||
return getActiveRawTeammateNameKeys(snapshot).length > 0;
|
||||
}
|
||||
|
||||
function hasRemovedRawMemberRoster(snapshot: TeamViewSnapshot | null | undefined): boolean {
|
||||
return Boolean(snapshot?.members.some((member) => member.removedAt));
|
||||
}
|
||||
|
||||
function hasConfigTeammateRoster(snapshot: TeamViewSnapshot | null | undefined): boolean {
|
||||
return Boolean(
|
||||
snapshot?.config.members?.some((member) => {
|
||||
const name = member.name?.trim();
|
||||
return Boolean(name) && !member.removedAt && !isLeadMember(member);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
interface SummaryFallbackMemberSource {
|
||||
name: string;
|
||||
agentId?: string;
|
||||
role?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
function normalizeSummaryTeammateName(
|
||||
name: string | undefined | null,
|
||||
leadName?: string
|
||||
): string | null {
|
||||
const trimmed = name?.trim();
|
||||
const normalizedName = trimmed?.toLowerCase();
|
||||
const normalizedLeadName = leadName?.trim().toLowerCase();
|
||||
if (
|
||||
!trimmed ||
|
||||
normalizedName === 'user' ||
|
||||
isLeadMember({ name: trimmed }) ||
|
||||
(normalizedLeadName && normalizedName === normalizedLeadName)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function getSummaryRosterTeammateSources(summary: TeamSummary): SummaryFallbackMemberSource[] {
|
||||
const seenNames = new Set<string>();
|
||||
const sources: SummaryFallbackMemberSource[] = [];
|
||||
for (const member of summary.members ?? []) {
|
||||
const name = normalizeSummaryTeammateName(member.name, summary.leadName);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const key = name.toLowerCase();
|
||||
if (seenNames.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seenNames.add(key);
|
||||
sources.push({
|
||||
name,
|
||||
agentId: member.agentId,
|
||||
role: member.role,
|
||||
color: member.color,
|
||||
});
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
function shouldUseSummaryLaunchTeammateSources(summary: TeamSummary): boolean {
|
||||
return (
|
||||
summary.partialLaunchFailure === true ||
|
||||
summary.teamLaunchState === 'partial_failure' ||
|
||||
summary.teamLaunchState === 'partial_pending' ||
|
||||
summary.teamLaunchState === 'partial_skipped'
|
||||
);
|
||||
}
|
||||
|
||||
function getSummaryLaunchTeammateSources(summary: TeamSummary): SummaryFallbackMemberSource[] {
|
||||
if (!shouldUseSummaryLaunchTeammateSources(summary)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const seenNames = new Set<string>();
|
||||
const sources: SummaryFallbackMemberSource[] = [];
|
||||
for (const rawName of [...(summary.missingMembers ?? []), ...(summary.skippedMembers ?? [])]) {
|
||||
const name = normalizeSummaryTeammateName(rawName, summary.leadName);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const key = name.toLowerCase();
|
||||
if (seenNames.has(key)) {
|
||||
continue;
|
||||
}
|
||||
seenNames.add(key);
|
||||
sources.push({ name });
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
function getSummaryLaunchTeammateNameKeys(summary: TeamSummary): string[] {
|
||||
return getSummaryLaunchTeammateSources(summary)
|
||||
.map((member) => member.name.toLowerCase())
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function getSummaryTeammateNameKeys(summary: TeamSummary): string[] {
|
||||
const rosterNames = getSummaryRosterTeammateSources(summary)
|
||||
.map((member) => member.name.toLowerCase())
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
if (rosterNames.length > 0) {
|
||||
return rosterNames;
|
||||
}
|
||||
|
||||
const launchNames = getSummaryLaunchTeammateNameKeys(summary);
|
||||
const expectedCount = summary.expectedMemberCount ?? summary.memberCount;
|
||||
if (expectedCount > 0 && launchNames.length === expectedCount) {
|
||||
return launchNames;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function getSummaryFallbackTeammateSources(summary: TeamSummary): SummaryFallbackMemberSource[] {
|
||||
return getSummaryRosterTeammateSources(summary);
|
||||
}
|
||||
|
||||
function areNameKeyListsEqual(left: readonly string[], right: readonly string[]): boolean {
|
||||
return left.length === right.length && left.every((name, index) => name === right[index]);
|
||||
}
|
||||
|
||||
function summaryConfirmsActiveTeammateRoster(
|
||||
current: TeamViewSnapshot,
|
||||
summary: TeamSummary
|
||||
): boolean {
|
||||
if ((summary.expectedMemberCount ?? summary.memberCount) <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentNames = getActiveRawTeammateNameKeys(current);
|
||||
const summaryNames = getSummaryTeammateNameKeys(summary);
|
||||
if (summaryNames.length === 0 || summaryNames.length !== currentNames.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return areNameKeyListsEqual(summaryNames, currentNames);
|
||||
}
|
||||
|
||||
function buildSummaryFallbackMemberSnapshots(
|
||||
snapshot: TeamViewSnapshot,
|
||||
summary: TeamSummary | undefined
|
||||
|
|
@ -1990,8 +2149,8 @@ function buildSummaryFallbackMemberSnapshots(
|
|||
if (!summary) {
|
||||
return [];
|
||||
}
|
||||
const summaryMembers = summary.members ?? [];
|
||||
if (summaryMembers.length === 0 || summary.memberCount <= 0) {
|
||||
const summaryMembers = getSummaryFallbackTeammateSources(summary);
|
||||
if (summaryMembers.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
@ -2021,11 +2180,7 @@ function buildSummaryFallbackMemberSnapshots(
|
|||
};
|
||||
|
||||
const teammates = summaryMembers.flatMap((member) => {
|
||||
const name = member.name?.trim();
|
||||
if (!name || name === 'user' || isLeadMember(member)) {
|
||||
return [];
|
||||
}
|
||||
const item = buildSnapshot(name, member);
|
||||
const item = buildSnapshot(member.name, member);
|
||||
return item ? [item] : [];
|
||||
});
|
||||
if (teammates.length === 0) {
|
||||
|
|
@ -2080,71 +2235,6 @@ function getResolvableMemberSnapshots(
|
|||
return snapshot.members;
|
||||
}
|
||||
|
||||
function getActiveRawTeammateNameKeys(snapshot: TeamViewSnapshot | null | undefined): string[] {
|
||||
if (!snapshot) {
|
||||
return [];
|
||||
}
|
||||
const names = new Set<string>();
|
||||
for (const member of snapshot.members) {
|
||||
const name = member.name.trim();
|
||||
if (!name || name === 'user' || member.removedAt || isLeadMember(member)) {
|
||||
continue;
|
||||
}
|
||||
names.add(name.toLowerCase());
|
||||
}
|
||||
return Array.from(names).sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function hasActiveRawTeammateRoster(snapshot: TeamViewSnapshot | null | undefined): boolean {
|
||||
return getActiveRawTeammateNameKeys(snapshot).length > 0;
|
||||
}
|
||||
|
||||
function hasRemovedRawMemberRoster(snapshot: TeamViewSnapshot | null | undefined): boolean {
|
||||
return Boolean(snapshot?.members.some((member) => member.removedAt));
|
||||
}
|
||||
|
||||
function hasConfigTeammateRoster(snapshot: TeamViewSnapshot | null | undefined): boolean {
|
||||
return Boolean(
|
||||
snapshot?.config.members?.some((member) => {
|
||||
const name = member.name?.trim();
|
||||
return Boolean(name) && !member.removedAt && !isLeadMember(member);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function getSummaryTeammateNameKeys(summary: TeamSummary): string[] {
|
||||
const names = new Set<string>();
|
||||
for (const member of summary.members ?? []) {
|
||||
const name = member.name?.trim();
|
||||
if (!name || name === 'user' || isLeadMember(member)) {
|
||||
continue;
|
||||
}
|
||||
names.add(name.toLowerCase());
|
||||
}
|
||||
return Array.from(names).sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
function areNameKeyListsEqual(left: readonly string[], right: readonly string[]): boolean {
|
||||
return left.length === right.length && left.every((name, index) => name === right[index]);
|
||||
}
|
||||
|
||||
function summaryConfirmsActiveTeammateRoster(
|
||||
current: TeamViewSnapshot,
|
||||
summary: TeamSummary
|
||||
): boolean {
|
||||
if (summary.memberCount <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentNames = getActiveRawTeammateNameKeys(current);
|
||||
const summaryNames = getSummaryTeammateNameKeys(summary);
|
||||
if (summaryNames.length === 0 || summaryNames.length !== currentNames.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return areNameKeyListsEqual(summaryNames, currentNames);
|
||||
}
|
||||
|
||||
function shouldPreserveSelectedTeamSnapshot(
|
||||
current: TeamViewSnapshot | null,
|
||||
baseline: TeamViewSnapshot | null | undefined,
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ function getRuntimeSelectorModels(
|
|||
return getVisibleTeamProviderModels(providerId, catalogModels, providerStatus);
|
||||
}
|
||||
|
||||
return sortTeamProviderModels(providerId, providerStatus.models);
|
||||
return sortTeamProviderModels(providerId, providerStatus.models, providerStatus);
|
||||
}
|
||||
|
||||
function getVisibleRuntimeModels(
|
||||
|
|
|
|||
|
|
@ -400,9 +400,38 @@ export function getRuntimeAwareTeamModelBadgeLabel(
|
|||
return getTeamModelBadgeLabel(providerId, model);
|
||||
}
|
||||
|
||||
function hasExplicitFreeOpenCodeModelMarker(model: string): boolean {
|
||||
const normalized = model.trim().toLowerCase();
|
||||
return (
|
||||
normalized === 'opencode/big-pickle' ||
|
||||
normalized.includes(':free') ||
|
||||
normalized.endsWith('-free') ||
|
||||
normalized.endsWith('/free')
|
||||
);
|
||||
}
|
||||
|
||||
function isFreeOpenCodeModelForOrdering(
|
||||
providerId: SupportedProviderId,
|
||||
model: string,
|
||||
providerStatus?: RuntimeAwareProviderStatus | null
|
||||
): boolean {
|
||||
if (providerId !== 'opencode') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const runtimeModel = getRuntimeCatalogModel(providerId, model, providerStatus);
|
||||
const badgeLabel = runtimeModel?.badgeLabel?.trim().toLowerCase();
|
||||
if (badgeLabel) {
|
||||
return badgeLabel === 'free';
|
||||
}
|
||||
|
||||
return hasExplicitFreeOpenCodeModelMarker(model);
|
||||
}
|
||||
|
||||
export function sortTeamProviderModels(
|
||||
providerId: SupportedProviderId,
|
||||
models: readonly string[]
|
||||
models: readonly string[],
|
||||
providerStatus?: RuntimeAwareProviderStatus | null
|
||||
): string[] {
|
||||
const seen = new Set<string>();
|
||||
const deduped = models.filter((model) => {
|
||||
|
|
@ -415,7 +444,7 @@ export function sortTeamProviderModels(
|
|||
});
|
||||
const order = TEAM_PROVIDER_MODEL_ORDER[providerId];
|
||||
|
||||
return [...deduped].sort((left, right) => {
|
||||
const sorted = [...deduped].sort((left, right) => {
|
||||
const leftRank = order.get(left) ?? Number.MAX_SAFE_INTEGER;
|
||||
const rightRank = order.get(right) ?? Number.MAX_SAFE_INTEGER;
|
||||
if (leftRank !== rightRank) {
|
||||
|
|
@ -423,6 +452,22 @@ export function sortTeamProviderModels(
|
|||
}
|
||||
return left.localeCompare(right);
|
||||
});
|
||||
|
||||
if (providerId !== 'opencode') {
|
||||
return sorted;
|
||||
}
|
||||
|
||||
return sorted
|
||||
.map((model, index) => ({ model, index }))
|
||||
.sort((left, right) => {
|
||||
const leftFree = isFreeOpenCodeModelForOrdering(providerId, left.model, providerStatus);
|
||||
const rightFree = isFreeOpenCodeModelForOrdering(providerId, right.model, providerStatus);
|
||||
if (leftFree !== rightFree) {
|
||||
return leftFree ? -1 : 1;
|
||||
}
|
||||
return left.index - right.index;
|
||||
})
|
||||
.map((entry) => entry.model);
|
||||
}
|
||||
|
||||
export function isCodexChatGptSubscriptionProviderStatus(
|
||||
|
|
@ -468,7 +513,11 @@ export function getVisibleTeamProviderModels(
|
|||
): string[] {
|
||||
return sortTeamProviderModels(
|
||||
providerId,
|
||||
filterVisibleProviderRuntimeModels(providerId, getSupplementalVisibleModels(providerId, models))
|
||||
filterVisibleProviderRuntimeModels(
|
||||
providerId,
|
||||
getSupplementalVisibleModels(providerId, models)
|
||||
),
|
||||
providerStatus
|
||||
).filter((model) => !isRuntimeHiddenTeamModel(providerId, model, providerStatus));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,6 +147,12 @@ export interface CliProviderModelCatalogItem {
|
|||
source: CliProviderModelCatalogSource;
|
||||
badgeLabel?: string | null;
|
||||
statusMessage?: string | null;
|
||||
metadata?: {
|
||||
cost?: unknown;
|
||||
context?: number | null;
|
||||
limits?: unknown;
|
||||
free?: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface CliProviderModelCatalog {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import type {
|
|||
} from '@features/codex-account/contracts';
|
||||
|
||||
const {
|
||||
apiKeyHasPreferredMock,
|
||||
apiKeyLookupMock,
|
||||
binaryResolveMock,
|
||||
detectLocalAccountStateMock,
|
||||
|
|
@ -25,6 +26,7 @@ const {
|
|||
readRateLimitsMock,
|
||||
} = vi.hoisted(() => ({
|
||||
binaryResolveMock: vi.fn(),
|
||||
apiKeyHasPreferredMock: vi.fn(),
|
||||
apiKeyLookupMock: vi.fn(),
|
||||
detectLocalAccountStateMock: vi.fn(),
|
||||
getCachedShellEnvMock: vi.fn(),
|
||||
|
|
@ -59,6 +61,7 @@ function emitLoginState(nextState: CodexLoginStateDto): void {
|
|||
|
||||
vi.mock('../../../../src/main/services/extensions', () => ({
|
||||
ApiKeyService: class MockApiKeyService {
|
||||
hasPreferred = apiKeyHasPreferredMock;
|
||||
lookupPreferred = apiKeyLookupMock;
|
||||
},
|
||||
}));
|
||||
|
|
@ -228,6 +231,7 @@ describe('createCodexAccountFeature', () => {
|
|||
delete process.env.OPENAI_API_KEY;
|
||||
delete process.env.CODEX_API_KEY;
|
||||
binaryResolveMock.mockResolvedValue('/usr/local/bin/codex');
|
||||
apiKeyHasPreferredMock.mockResolvedValue(false);
|
||||
apiKeyLookupMock.mockResolvedValue(null);
|
||||
detectLocalAccountStateMock.mockResolvedValue({
|
||||
hasArtifacts: false,
|
||||
|
|
|
|||
|
|
@ -36,21 +36,22 @@ describe('ApiKeyService', () => {
|
|||
});
|
||||
|
||||
it('persists projectPath for project-scoped API keys', async () => {
|
||||
const projectPath = path.join(tempDir, 'project-a');
|
||||
const saved = await service.save({
|
||||
name: 'Project Tavily',
|
||||
envVarName: 'TAVILY_API_KEY',
|
||||
value: 'secret',
|
||||
scope: 'project',
|
||||
projectPath: '/tmp/project-a',
|
||||
projectPath,
|
||||
});
|
||||
|
||||
expect(saved.scope).toBe('project');
|
||||
expect(saved.projectPath).toBe('/tmp/project-a');
|
||||
expect(saved.projectPath).toBe(projectPath);
|
||||
|
||||
await expect(service.list()).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
scope: 'project',
|
||||
projectPath: '/tmp/project-a',
|
||||
projectPath,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
|
@ -67,6 +68,7 @@ describe('ApiKeyService', () => {
|
|||
});
|
||||
|
||||
it('prefers exact project matches over user keys during lookup', async () => {
|
||||
const projectPath = path.join(tempDir, 'project-a');
|
||||
await service.save({
|
||||
name: 'Shared Tavily',
|
||||
envVarName: 'TAVILY_API_KEY',
|
||||
|
|
@ -78,10 +80,10 @@ describe('ApiKeyService', () => {
|
|||
envVarName: 'TAVILY_API_KEY',
|
||||
value: 'project-secret',
|
||||
scope: 'project',
|
||||
projectPath: '/tmp/project-a',
|
||||
projectPath,
|
||||
});
|
||||
|
||||
await expect(service.lookup(['TAVILY_API_KEY'], '/tmp/project-a')).resolves.toEqual([
|
||||
await expect(service.lookup(['TAVILY_API_KEY'], projectPath)).resolves.toEqual([
|
||||
{
|
||||
envVarName: 'TAVILY_API_KEY',
|
||||
value: 'project-secret',
|
||||
|
|
@ -90,6 +92,8 @@ describe('ApiKeyService', () => {
|
|||
});
|
||||
|
||||
it('falls back to user keys when project-specific matches do not exist', async () => {
|
||||
const projectPath = path.join(tempDir, 'project-a');
|
||||
const otherProjectPath = path.join(tempDir, 'project-b');
|
||||
await service.save({
|
||||
name: 'Shared Tavily',
|
||||
envVarName: 'TAVILY_API_KEY',
|
||||
|
|
@ -101,10 +105,10 @@ describe('ApiKeyService', () => {
|
|||
envVarName: 'TAVILY_API_KEY',
|
||||
value: 'project-secret',
|
||||
scope: 'project',
|
||||
projectPath: '/tmp/project-b',
|
||||
projectPath: otherProjectPath,
|
||||
});
|
||||
|
||||
await expect(service.lookup(['TAVILY_API_KEY'], '/tmp/project-a')).resolves.toEqual([
|
||||
await expect(service.lookup(['TAVILY_API_KEY'], projectPath)).resolves.toEqual([
|
||||
{
|
||||
envVarName: 'TAVILY_API_KEY',
|
||||
value: 'user-secret',
|
||||
|
|
@ -113,18 +117,38 @@ describe('ApiKeyService', () => {
|
|||
});
|
||||
|
||||
it('does not leak project-scoped keys without project context', async () => {
|
||||
const projectPath = path.join(tempDir, 'project-a');
|
||||
await service.save({
|
||||
name: 'Project only key',
|
||||
envVarName: 'TAVILY_API_KEY',
|
||||
value: 'project-secret',
|
||||
scope: 'project',
|
||||
projectPath: '/tmp/project-a',
|
||||
projectPath,
|
||||
});
|
||||
|
||||
await expect(service.lookup(['TAVILY_API_KEY'])).resolves.toEqual([]);
|
||||
await expect(service.lookupPreferred('TAVILY_API_KEY')).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('checks preferred key presence without decrypting the stored value', async () => {
|
||||
vi.mocked(safeStorage.isEncryptionAvailable).mockReturnValue(true);
|
||||
vi.mocked(safeStorage.getSelectedStorageBackend).mockReturnValue('gnome_libsecret');
|
||||
vi.mocked(safeStorage.encryptString).mockReturnValue(Buffer.from('encrypted-value'));
|
||||
|
||||
await service.save({
|
||||
name: 'Anthropic API Key',
|
||||
envVarName: 'ANTHROPIC_API_KEY',
|
||||
value: 'secret',
|
||||
scope: 'user',
|
||||
});
|
||||
vi.mocked(safeStorage.decryptString).mockImplementation(() => {
|
||||
throw new Error('decrypt should not be called');
|
||||
});
|
||||
|
||||
await expect(service.hasPreferred('ANTHROPIC_API_KEY')).resolves.toBe(true);
|
||||
expect(safeStorage.decryptString).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not print decrypt failures to the normal console', async () => {
|
||||
vi.mocked(safeStorage.isEncryptionAvailable).mockReturnValue(true);
|
||||
vi.mocked(safeStorage.getSelectedStorageBackend).mockReturnValue('gnome_libsecret');
|
||||
|
|
|
|||
|
|
@ -1,17 +1,70 @@
|
|||
import { createHash } from 'crypto';
|
||||
import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { gzipSync } from 'zlib';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { resolveAppManagedOpenCodeRuntimeBinaryPath } from '@main/services/infrastructure/OpenCodeRuntimeInstallerService';
|
||||
const execCliMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@main/utils/childProcess', () => ({
|
||||
execCli: execCliMock,
|
||||
}));
|
||||
|
||||
import {
|
||||
extractOpenCodeRuntimeBinaryFromTarball,
|
||||
getOpenCodeRuntimePlatformCandidates,
|
||||
resolveAppManagedOpenCodeRuntimeBinaryPath,
|
||||
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath,
|
||||
verifyOpenCodeRuntimePackageIntegrity,
|
||||
} from '@main/services/infrastructure/OpenCodeRuntimeInstallerService';
|
||||
import { setAppDataBasePath } from '@main/utils/pathDecoder';
|
||||
|
||||
let tempRoot: string | null = null;
|
||||
|
||||
function writeOctal(header: Buffer, offset: number, length: number, value: number): void {
|
||||
const encoded = value
|
||||
.toString(8)
|
||||
.padStart(length - 1, '0')
|
||||
.slice(-(length - 1));
|
||||
header.write(`${encoded}\0`, offset, length, 'ascii');
|
||||
}
|
||||
|
||||
function createTarEntry(name: string, data: Buffer): Buffer {
|
||||
const header = Buffer.alloc(512);
|
||||
header.write(name, 0, Math.min(Buffer.byteLength(name), 100), 'utf8');
|
||||
writeOctal(header, 100, 8, 0o755);
|
||||
writeOctal(header, 108, 8, 0);
|
||||
writeOctal(header, 116, 8, 0);
|
||||
writeOctal(header, 124, 12, data.length);
|
||||
writeOctal(header, 136, 12, 0);
|
||||
header.fill(' ', 148, 156);
|
||||
header.write('0', 156, 1, 'ascii');
|
||||
header.write('ustar\0', 257, 6, 'ascii');
|
||||
header.write('00', 263, 2, 'ascii');
|
||||
const checksum = header.reduce((sum, byte) => sum + byte, 0);
|
||||
const checksumText = checksum.toString(8).padStart(6, '0');
|
||||
header.write(`${checksumText}\0 `, 148, 8, 'ascii');
|
||||
|
||||
const padding = Buffer.alloc((512 - (data.length % 512)) % 512);
|
||||
return Buffer.concat([header, data, padding]);
|
||||
}
|
||||
|
||||
function createTarball(entries: { name: string; data: string }[]): Buffer {
|
||||
return gzipSync(
|
||||
Buffer.concat([
|
||||
...entries.map((entry) => createTarEntry(entry.name, Buffer.from(entry.data))),
|
||||
Buffer.alloc(1024),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
describe('OpenCodeRuntimeInstallerService resolver', () => {
|
||||
beforeEach(async () => {
|
||||
tempRoot = await mkdtemp(path.join(os.tmpdir(), 'opencode-runtime-resolver-'));
|
||||
setAppDataBasePath(tempRoot);
|
||||
execCliMock.mockReset();
|
||||
execCliMock.mockResolvedValue({ stdout: 'opencode 1.0.0\n', stderr: '' });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
|
@ -71,4 +124,111 @@ describe('OpenCodeRuntimeInstallerService resolver', () => {
|
|||
|
||||
expect(resolveAppManagedOpenCodeRuntimeBinaryPath()).toBeNull();
|
||||
});
|
||||
|
||||
it('returns the verified app-managed binary path only when --version succeeds', async () => {
|
||||
const binaryPath = path.join(
|
||||
tempRoot!,
|
||||
'data',
|
||||
'runtimes',
|
||||
'opencode',
|
||||
'versions',
|
||||
'1.0.0',
|
||||
'opencode-test',
|
||||
'opencode'
|
||||
);
|
||||
const manifestPath = path.join(tempRoot!, 'data', 'runtimes', 'opencode', 'current.json');
|
||||
await mkdir(path.dirname(binaryPath), { recursive: true });
|
||||
await mkdir(path.dirname(manifestPath), { recursive: true });
|
||||
await writeFile(binaryPath, 'binary', { mode: 0o755 });
|
||||
await writeFile(
|
||||
manifestPath,
|
||||
`${JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
version: '1.0.0',
|
||||
platformPackage: 'opencode-test',
|
||||
binaryPath,
|
||||
integrity: 'sha512-test',
|
||||
installedAt: '2026-05-12T00:00:00.000Z',
|
||||
})}\n`,
|
||||
'utf8'
|
||||
);
|
||||
|
||||
await expect(resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()).resolves.toBe(binaryPath);
|
||||
expect(execCliMock).toHaveBeenCalledWith(binaryPath, ['--version'], {
|
||||
timeout: 10_000,
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
execCliMock.mockRejectedValueOnce(new Error('broken binary'));
|
||||
|
||||
await expect(resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath()).resolves.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('OpenCodeRuntimeInstallerService package safety helpers', () => {
|
||||
it('selects expected platform packages with Linux musl and baseline fallbacks', () => {
|
||||
expect(
|
||||
getOpenCodeRuntimePlatformCandidates('darwin', 'arm64', false).map((item) => item.packageName)
|
||||
).toEqual(['opencode-darwin-arm64']);
|
||||
expect(
|
||||
getOpenCodeRuntimePlatformCandidates('darwin', 'x64', false).map((item) => item.packageName)
|
||||
).toEqual(['opencode-darwin-x64', 'opencode-darwin-x64-baseline']);
|
||||
expect(
|
||||
getOpenCodeRuntimePlatformCandidates('linux', 'x64', false).map((item) => item.packageName)
|
||||
).toEqual(['opencode-linux-x64', 'opencode-linux-x64-baseline', 'opencode-linux-x64-musl']);
|
||||
expect(
|
||||
getOpenCodeRuntimePlatformCandidates('linux', 'x64', true).map((item) => item.packageName)
|
||||
).toEqual([
|
||||
'opencode-linux-x64-musl',
|
||||
'opencode-linux-x64-baseline-musl',
|
||||
'opencode-linux-x64',
|
||||
]);
|
||||
expect(
|
||||
getOpenCodeRuntimePlatformCandidates('linux', 'arm64', false).map((item) => item.packageName)
|
||||
).toEqual(['opencode-linux-arm64', 'opencode-linux-arm64-musl']);
|
||||
expect(
|
||||
getOpenCodeRuntimePlatformCandidates('linux', 'arm64', true).map((item) => item.packageName)
|
||||
).toEqual(['opencode-linux-arm64-musl', 'opencode-linux-arm64']);
|
||||
expect(
|
||||
getOpenCodeRuntimePlatformCandidates('win32', 'x64', false).map((item) => item.packageName)
|
||||
).toEqual(['opencode-windows-x64', 'opencode-windows-x64-baseline']);
|
||||
expect(
|
||||
getOpenCodeRuntimePlatformCandidates('win32', 'arm64', false).map((item) => item.packageName)
|
||||
).toEqual(['opencode-windows-arm64']);
|
||||
});
|
||||
|
||||
it('fails npm integrity mismatches', () => {
|
||||
const payload = Buffer.from('actual package');
|
||||
const wrongHash = createHash('sha512').update('different package').digest('base64');
|
||||
|
||||
expect(() => verifyOpenCodeRuntimePackageIntegrity(payload, `sha512-${wrongHash}`)).toThrow(
|
||||
'integrity check failed'
|
||||
);
|
||||
});
|
||||
|
||||
it('extracts only the expected OpenCode binary from the package tarball', () => {
|
||||
const tarball = createTarball([
|
||||
{ name: 'package/bin/not-opencode', data: 'wrong' },
|
||||
{
|
||||
name: process.platform === 'win32' ? 'package/bin/opencode.exe' : 'package/bin/opencode',
|
||||
data: 'right',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(extractOpenCodeRuntimeBinaryFromTarball(tarball).toString()).toBe('right');
|
||||
});
|
||||
|
||||
it('rejects tar path traversal before extraction', () => {
|
||||
const tarball = createTarball([
|
||||
{ name: '../opencode', data: 'unsafe' },
|
||||
{
|
||||
name: process.platform === 'win32' ? 'package/bin/opencode.exe' : 'package/bin/opencode',
|
||||
data: 'right',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(() => extractOpenCodeRuntimeBinaryFromTarball(tarball)).toThrow(
|
||||
'Unsafe OpenCode package tar entry'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -320,6 +320,11 @@ describe('ClaudeMultimodelBridgeService', () => {
|
|||
'runtime status --json --provider opencode',
|
||||
])
|
||||
);
|
||||
expect(
|
||||
execCliMock.mock.calls
|
||||
.filter((call) => call[1].join(' ').startsWith('runtime status --json --provider '))
|
||||
.map((call) => call[2]?.maxBuffer)
|
||||
).toEqual([8 * 1024 * 1024, 8 * 1024 * 1024, 8 * 1024 * 1024, 8 * 1024 * 1024]);
|
||||
expect(providers.map((provider) => provider.providerId)).toEqual([
|
||||
'anthropic',
|
||||
'codex',
|
||||
|
|
@ -448,6 +453,12 @@ describe('ClaudeMultimodelBridgeService', () => {
|
|||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
badgeLabel: 'Opus 4.8',
|
||||
metadata: {
|
||||
cost: { input: 0, output: 0 },
|
||||
context: 200000,
|
||||
limits: { context: 200000, output: 32000 },
|
||||
free: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'opus[1m]',
|
||||
|
|
@ -539,6 +550,12 @@ describe('ClaudeMultimodelBridgeService', () => {
|
|||
hidden: false,
|
||||
source: 'anthropic-models-api',
|
||||
badgeLabel: 'Opus 4.8',
|
||||
metadata: {
|
||||
cost: { input: 0, output: 0 },
|
||||
context: 200000,
|
||||
limits: { context: 200000, output: 32000 },
|
||||
free: true,
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
launchModel: 'opus[1m]',
|
||||
|
|
|
|||
|
|
@ -107,6 +107,54 @@ describe('ProviderConnectionService', () => {
|
|||
expect(result.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not decrypt stored Anthropic keys when metadata-only env building is requested', async () => {
|
||||
const lookupPreferred = vi.fn().mockResolvedValue({
|
||||
envVarName: 'ANTHROPIC_API_KEY',
|
||||
value: 'stored-key',
|
||||
});
|
||||
const { ProviderConnectionService } =
|
||||
await import('@main/services/runtime/ProviderConnectionService');
|
||||
|
||||
const service = new ProviderConnectionService(
|
||||
{
|
||||
lookupPreferred,
|
||||
} as never,
|
||||
{
|
||||
getConfig: () => createConfig('api_key'),
|
||||
} as never
|
||||
);
|
||||
|
||||
const result = await service.applyConfiguredConnectionEnv({}, 'anthropic', undefined, {
|
||||
allowStoredApiKeyDecryption: false,
|
||||
});
|
||||
|
||||
expect(lookupPreferred).not.toHaveBeenCalled();
|
||||
expect(result.ANTHROPIC_API_KEY).toBeUndefined();
|
||||
});
|
||||
|
||||
it('injects stored Gemini API keys for runtime launches', async () => {
|
||||
const lookupPreferred = vi.fn().mockResolvedValue({
|
||||
envVarName: 'GEMINI_API_KEY',
|
||||
value: 'gemini-stored-key',
|
||||
});
|
||||
const { ProviderConnectionService } =
|
||||
await import('@main/services/runtime/ProviderConnectionService');
|
||||
|
||||
const service = new ProviderConnectionService(
|
||||
{
|
||||
lookupPreferred,
|
||||
} as never,
|
||||
{
|
||||
getConfig: () => createConfig('auto'),
|
||||
} as never
|
||||
);
|
||||
|
||||
const result = await service.applyConfiguredConnectionEnv({}, 'gemini');
|
||||
|
||||
expect(lookupPreferred).toHaveBeenCalledWith('GEMINI_API_KEY');
|
||||
expect(result.GEMINI_API_KEY).toBe('gemini-stored-key');
|
||||
});
|
||||
|
||||
it('reports a missing Anthropic API key when api_key mode is selected', async () => {
|
||||
const { ProviderConnectionService } =
|
||||
await import('@main/services/runtime/ProviderConnectionService');
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ const applyConfiguredConnectionEnvMock = vi.fn();
|
|||
const applyAllConfiguredConnectionEnvMock = vi.fn();
|
||||
const getConfiguredConnectionIssuesMock = vi.fn();
|
||||
const getConfiguredConnectionLaunchArgsMock = vi.fn();
|
||||
const resolveVerifiedAppManagedOpenCodeRuntimeBinaryPathMock = vi.fn();
|
||||
|
||||
vi.mock('@main/utils/cliEnv', () => ({
|
||||
buildEnrichedEnv: (...args: Parameters<typeof buildEnrichedEnvMock>) => buildEnrichedEnvMock(...args),
|
||||
buildEnrichedEnv: (...args: Parameters<typeof buildEnrichedEnvMock>) =>
|
||||
buildEnrichedEnvMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@main/utils/shellEnv', () => ({
|
||||
|
|
@ -35,22 +37,31 @@ vi.mock('../../../../src/main/services/infrastructure/ConfigManager', () => ({
|
|||
|
||||
vi.mock('../../../../src/main/services/runtime/ProviderConnectionService', () => ({
|
||||
providerConnectionService: {
|
||||
augmentConfiguredConnectionEnv: (...args: Parameters<typeof augmentConfiguredConnectionEnvMock>) =>
|
||||
augmentConfiguredConnectionEnvMock(...args),
|
||||
augmentAllConfiguredConnectionEnv: (...args: Parameters<typeof augmentAllConfiguredConnectionEnvMock>) =>
|
||||
augmentAllConfiguredConnectionEnvMock(...args),
|
||||
augmentConfiguredConnectionEnv: (
|
||||
...args: Parameters<typeof augmentConfiguredConnectionEnvMock>
|
||||
) => augmentConfiguredConnectionEnvMock(...args),
|
||||
augmentAllConfiguredConnectionEnv: (
|
||||
...args: Parameters<typeof augmentAllConfiguredConnectionEnvMock>
|
||||
) => augmentAllConfiguredConnectionEnvMock(...args),
|
||||
applyConfiguredConnectionEnv: (...args: Parameters<typeof applyConfiguredConnectionEnvMock>) =>
|
||||
applyConfiguredConnectionEnvMock(...args),
|
||||
applyAllConfiguredConnectionEnv: (...args: Parameters<typeof applyAllConfiguredConnectionEnvMock>) =>
|
||||
applyAllConfiguredConnectionEnvMock(...args),
|
||||
applyAllConfiguredConnectionEnv: (
|
||||
...args: Parameters<typeof applyAllConfiguredConnectionEnvMock>
|
||||
) => applyAllConfiguredConnectionEnvMock(...args),
|
||||
getConfiguredConnectionLaunchArgs: (
|
||||
...args: Parameters<typeof getConfiguredConnectionLaunchArgsMock>
|
||||
) => getConfiguredConnectionLaunchArgsMock(...args),
|
||||
getConfiguredConnectionIssues: (...args: Parameters<typeof getConfiguredConnectionIssuesMock>) =>
|
||||
getConfiguredConnectionIssuesMock(...args),
|
||||
getConfiguredConnectionIssues: (
|
||||
...args: Parameters<typeof getConfiguredConnectionIssuesMock>
|
||||
) => getConfiguredConnectionIssuesMock(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../../src/main/services/infrastructure/OpenCodeRuntimeInstallerService', () => ({
|
||||
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPath: () =>
|
||||
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPathMock(),
|
||||
}));
|
||||
|
||||
describe('buildProviderAwareCliEnv', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
|
|
@ -76,6 +87,7 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
);
|
||||
getConfiguredConnectionLaunchArgsMock.mockResolvedValue([]);
|
||||
getConfiguredConnectionIssuesMock.mockResolvedValue({});
|
||||
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPathMock.mockResolvedValue(null);
|
||||
});
|
||||
|
||||
it('builds provider-pinned CLI env and returns provider-specific issues', async () => {
|
||||
|
|
@ -83,9 +95,8 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
anthropic: 'missing key',
|
||||
});
|
||||
|
||||
const { buildProviderAwareCliEnv } = await import(
|
||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
||||
);
|
||||
const { buildProviderAwareCliEnv } =
|
||||
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||
const result = await buildProviderAwareCliEnv({
|
||||
binaryPath: '/mock/claude',
|
||||
providerId: 'anthropic',
|
||||
|
|
@ -112,10 +123,27 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
expect(result.providerArgs).toEqual([]);
|
||||
});
|
||||
|
||||
it('builds shared env for generic CLI launches when no provider is specified', async () => {
|
||||
const { buildProviderAwareCliEnv } = await import(
|
||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
||||
it('passes metadata-only stored API key access through provider env building', async () => {
|
||||
const { buildProviderAwareCliEnv } =
|
||||
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||
await buildProviderAwareCliEnv({
|
||||
providerId: 'anthropic',
|
||||
allowStoredApiKeyDecryption: false,
|
||||
});
|
||||
|
||||
expect(applyConfiguredConnectionEnvMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
CLAUDE_CODE_ENTRY_PROVIDER: 'anthropic',
|
||||
}),
|
||||
'anthropic',
|
||||
undefined,
|
||||
{ allowStoredApiKeyDecryption: false }
|
||||
);
|
||||
});
|
||||
|
||||
it('builds shared env for generic CLI launches when no provider is specified', async () => {
|
||||
const { buildProviderAwareCliEnv } =
|
||||
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||
const result = await buildProviderAwareCliEnv();
|
||||
|
||||
expect(applyAllConfiguredConnectionEnvMock).toHaveBeenCalledWith(
|
||||
|
|
@ -140,9 +168,8 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
PATH: '/usr/bin',
|
||||
OPENCODE_DISABLE_AUTOUPDATE: '1',
|
||||
});
|
||||
const { buildProviderAwareCliEnv } = await import(
|
||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
||||
);
|
||||
const { buildProviderAwareCliEnv } =
|
||||
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||
|
||||
const result = await buildProviderAwareCliEnv({
|
||||
env: {
|
||||
|
|
@ -155,9 +182,8 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
});
|
||||
|
||||
it('uses non-destructive credential augmentation for PTY-style envs', async () => {
|
||||
const { buildProviderAwareCliEnv } = await import(
|
||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
||||
);
|
||||
const { buildProviderAwareCliEnv } =
|
||||
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||
const result = await buildProviderAwareCliEnv({
|
||||
connectionMode: 'augment',
|
||||
env: {
|
||||
|
|
@ -176,9 +202,8 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
});
|
||||
|
||||
it('preserves caller-provided HOME and USERPROFILE overrides', async () => {
|
||||
const { buildProviderAwareCliEnv } = await import(
|
||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
||||
);
|
||||
const { buildProviderAwareCliEnv } =
|
||||
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||
const result = await buildProviderAwareCliEnv({
|
||||
providerId: 'anthropic',
|
||||
env: {
|
||||
|
|
@ -201,9 +226,8 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
});
|
||||
|
||||
it('preserves explicit backend overrides passed by the caller', async () => {
|
||||
const { buildProviderAwareCliEnv } = await import(
|
||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
||||
);
|
||||
const { buildProviderAwareCliEnv } =
|
||||
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||
const result = await buildProviderAwareCliEnv({
|
||||
connectionMode: 'augment',
|
||||
env: {
|
||||
|
|
@ -227,9 +251,8 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
PATH: '/usr/bin',
|
||||
});
|
||||
|
||||
const { buildProviderAwareCliEnv } = await import(
|
||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
||||
);
|
||||
const { buildProviderAwareCliEnv } =
|
||||
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||
const result = await buildProviderAwareCliEnv({
|
||||
providerId: 'codex',
|
||||
});
|
||||
|
|
@ -251,9 +274,8 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
'{"codex":{"forced_login_method":"chatgpt"}}',
|
||||
]);
|
||||
|
||||
const { buildProviderAwareCliEnv } = await import(
|
||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
||||
);
|
||||
const { buildProviderAwareCliEnv } =
|
||||
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||
const result = await buildProviderAwareCliEnv({
|
||||
binaryPath: '/mock/claude-multimodel',
|
||||
providerId: 'codex',
|
||||
|
|
@ -272,4 +294,42 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
'{"codex":{"forced_login_method":"chatgpt"}}',
|
||||
]);
|
||||
});
|
||||
|
||||
it('injects the verified app-managed OpenCode binary for OpenCode launches', async () => {
|
||||
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPathMock.mockResolvedValue(
|
||||
'/Users/tester/App Support/runtimes/opencode/current/opencode'
|
||||
);
|
||||
|
||||
const { buildProviderAwareCliEnv } =
|
||||
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||
const result = await buildProviderAwareCliEnv({
|
||||
providerId: 'opencode',
|
||||
});
|
||||
|
||||
expect(applyConfiguredConnectionEnvMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH:
|
||||
'/Users/tester/App Support/runtimes/opencode/current/opencode',
|
||||
}),
|
||||
'opencode',
|
||||
undefined
|
||||
);
|
||||
expect(result.env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBe(
|
||||
'/Users/tester/App Support/runtimes/opencode/current/opencode'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not inject the app-managed OpenCode binary into non-OpenCode provider launches', async () => {
|
||||
resolveVerifiedAppManagedOpenCodeRuntimeBinaryPathMock.mockResolvedValue(
|
||||
'/Users/tester/App Support/runtimes/opencode/current/opencode'
|
||||
);
|
||||
|
||||
const { buildProviderAwareCliEnv } =
|
||||
await import('../../../../src/main/services/runtime/providerAwareCliEnv');
|
||||
const result = await buildProviderAwareCliEnv({
|
||||
providerId: 'anthropic',
|
||||
});
|
||||
|
||||
expect(result.env.CLAUDE_MULTIMODEL_OPENCODE_BIN_PATH).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -308,9 +308,7 @@ describe('OpenCodeReadinessBridge', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('recovers accepted OpenCode sendMessage after bridge timeout through commandStatus when enabled', async () => {
|
||||
const previous = process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY;
|
||||
process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY = '1';
|
||||
it('recovers accepted OpenCode sendMessage after bridge timeout through commandStatus by default', async () => {
|
||||
const executor = fakeSequenceExecutor([
|
||||
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
||||
bridgeCommandSuccess({
|
||||
|
|
@ -328,34 +326,26 @@ describe('OpenCodeReadinessBridge', () => {
|
|||
]);
|
||||
const bridge = new OpenCodeReadinessBridge(executor);
|
||||
|
||||
try {
|
||||
await expect(
|
||||
bridge.sendOpenCodeTeamMessage({
|
||||
teamId: 'team-a',
|
||||
teamName: 'team-a',
|
||||
laneId: 'secondary:opencode:bob',
|
||||
projectPath: '/repo',
|
||||
memberName: 'bob',
|
||||
text: 'hello',
|
||||
messageId: 'message-1',
|
||||
deliveryAttemptId: 'ledger-1:1:payload',
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
accepted: true,
|
||||
sessionId: 'session-bob',
|
||||
diagnostics: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: 'opencode_send_recovered_after_bridge_timeout',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY;
|
||||
} else {
|
||||
process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY = previous;
|
||||
}
|
||||
}
|
||||
await expect(
|
||||
bridge.sendOpenCodeTeamMessage({
|
||||
teamId: 'team-a',
|
||||
teamName: 'team-a',
|
||||
laneId: 'secondary:opencode:bob',
|
||||
projectPath: '/repo',
|
||||
memberName: 'bob',
|
||||
text: 'hello',
|
||||
messageId: 'message-1',
|
||||
deliveryAttemptId: 'ledger-1:1:payload',
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
accepted: true,
|
||||
sessionId: 'session-bob',
|
||||
diagnostics: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
code: 'opencode_send_recovered_after_bridge_timeout',
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
expect(executor.execute).toHaveBeenCalledTimes(2);
|
||||
const sendOptions = executor.execute.mock.calls[0]?.[2] as { requestId?: string } | undefined;
|
||||
|
|
@ -375,152 +365,142 @@ describe('OpenCodeReadinessBridge', () => {
|
|||
});
|
||||
|
||||
it('does not query commandStatus for non-timeout OpenCode sendMessage failures', async () => {
|
||||
await withCommandStatusRecoveryEnabled(async () => {
|
||||
const executor = fakeExecutor(
|
||||
bridgeFailure('provider_error', 'OpenCode send failed', [])
|
||||
);
|
||||
const bridge = new OpenCodeReadinessBridge(executor);
|
||||
const executor = fakeExecutor(bridgeFailure('provider_error', 'OpenCode send failed', []));
|
||||
const bridge = new OpenCodeReadinessBridge(executor);
|
||||
|
||||
await expect(
|
||||
bridge.sendOpenCodeTeamMessage({
|
||||
teamId: 'team-a',
|
||||
teamName: 'team-a',
|
||||
laneId: 'secondary:opencode:bob',
|
||||
projectPath: '/repo',
|
||||
memberName: 'bob',
|
||||
text: 'hello',
|
||||
messageId: 'message-1',
|
||||
deliveryAttemptId: 'ledger-1:1:payload',
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
accepted: false,
|
||||
await expect(
|
||||
bridge.sendOpenCodeTeamMessage({
|
||||
teamId: 'team-a',
|
||||
teamName: 'team-a',
|
||||
laneId: 'secondary:opencode:bob',
|
||||
projectPath: '/repo',
|
||||
memberName: 'bob',
|
||||
diagnostics: [
|
||||
expect.objectContaining({
|
||||
code: 'provider_error',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(executor.execute).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the old send failure path when timeout commandStatus is unknown', async () => {
|
||||
await withCommandStatusRecoveryEnabled(async () => {
|
||||
const executor = fakeSequenceExecutor([
|
||||
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
||||
bridgeCommandSuccess({
|
||||
command: 'opencode.commandStatus',
|
||||
requestId: 'status-req-1',
|
||||
data: {
|
||||
status: 'unknown',
|
||||
safeToRetry: false,
|
||||
accepted: false,
|
||||
diagnostics: ['No orchestrator-side command outcome record matched the requested OpenCode command.'],
|
||||
},
|
||||
text: 'hello',
|
||||
messageId: 'message-1',
|
||||
deliveryAttemptId: 'ledger-1:1:payload',
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
accepted: false,
|
||||
memberName: 'bob',
|
||||
diagnostics: [
|
||||
expect.objectContaining({
|
||||
code: 'provider_error',
|
||||
}),
|
||||
]);
|
||||
const bridge = new OpenCodeReadinessBridge(executor);
|
||||
|
||||
await expect(
|
||||
bridge.sendOpenCodeTeamMessage({
|
||||
teamId: 'team-a',
|
||||
teamName: 'team-a',
|
||||
laneId: 'secondary:opencode:bob',
|
||||
projectPath: '/repo',
|
||||
memberName: 'bob',
|
||||
text: 'hello',
|
||||
messageId: 'message-1',
|
||||
deliveryAttemptId: 'ledger-1:1:payload',
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
accepted: false,
|
||||
memberName: 'bob',
|
||||
diagnostics: [
|
||||
expect.objectContaining({
|
||||
code: 'timeout',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(executor.execute).toHaveBeenCalledTimes(2);
|
||||
],
|
||||
});
|
||||
|
||||
expect(executor.execute).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('keeps the old send failure path when timeout commandStatus is unavailable', async () => {
|
||||
await withCommandStatusRecoveryEnabled(async () => {
|
||||
const executor = fakeSequenceExecutor([
|
||||
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
||||
bridgeFailure('timeout', 'OpenCode commandStatus timed out', []),
|
||||
]);
|
||||
const bridge = new OpenCodeReadinessBridge(executor);
|
||||
it('keeps the timeout failure path when timeout commandStatus is unknown', async () => {
|
||||
const executor = fakeSequenceExecutor([
|
||||
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
||||
bridgeCommandSuccess({
|
||||
command: 'opencode.commandStatus',
|
||||
requestId: 'status-req-1',
|
||||
data: {
|
||||
status: 'unknown',
|
||||
safeToRetry: false,
|
||||
accepted: false,
|
||||
diagnostics: ['No orchestrator-side command outcome record matched the requested OpenCode command.'],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const bridge = new OpenCodeReadinessBridge(executor);
|
||||
|
||||
await expect(
|
||||
bridge.sendOpenCodeTeamMessage({
|
||||
teamId: 'team-a',
|
||||
teamName: 'team-a',
|
||||
laneId: 'secondary:opencode:bob',
|
||||
projectPath: '/repo',
|
||||
memberName: 'bob',
|
||||
text: 'hello',
|
||||
messageId: 'message-1',
|
||||
deliveryAttemptId: 'ledger-1:1:payload',
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
accepted: false,
|
||||
await expect(
|
||||
bridge.sendOpenCodeTeamMessage({
|
||||
teamId: 'team-a',
|
||||
teamName: 'team-a',
|
||||
laneId: 'secondary:opencode:bob',
|
||||
projectPath: '/repo',
|
||||
memberName: 'bob',
|
||||
diagnostics: [
|
||||
expect.objectContaining({
|
||||
code: 'timeout',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(executor.execute).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the old send failure path when timeout commandStatus reports precondition mismatch', async () => {
|
||||
await withCommandStatusRecoveryEnabled(async () => {
|
||||
const executor = fakeSequenceExecutor([
|
||||
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
||||
bridgeCommandSuccess({
|
||||
command: 'opencode.commandStatus',
|
||||
requestId: 'status-req-1',
|
||||
data: {
|
||||
status: 'precondition_mismatch',
|
||||
safeToRetry: false,
|
||||
accepted: false,
|
||||
diagnostics: ['OpenCode command status payloadHash mismatch.'],
|
||||
},
|
||||
text: 'hello',
|
||||
messageId: 'message-1',
|
||||
deliveryAttemptId: 'ledger-1:1:payload',
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
accepted: false,
|
||||
memberName: 'bob',
|
||||
diagnostics: [
|
||||
expect.objectContaining({
|
||||
code: 'timeout',
|
||||
}),
|
||||
]);
|
||||
const bridge = new OpenCodeReadinessBridge(executor);
|
||||
|
||||
await expect(
|
||||
bridge.sendOpenCodeTeamMessage({
|
||||
teamId: 'team-a',
|
||||
teamName: 'team-a',
|
||||
laneId: 'secondary:opencode:bob',
|
||||
projectPath: '/repo',
|
||||
memberName: 'bob',
|
||||
text: 'hello',
|
||||
messageId: 'message-1',
|
||||
deliveryAttemptId: 'ledger-1:1:payload',
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
accepted: false,
|
||||
memberName: 'bob',
|
||||
diagnostics: [
|
||||
expect.objectContaining({
|
||||
code: 'timeout',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(executor.execute).toHaveBeenCalledTimes(2);
|
||||
],
|
||||
});
|
||||
|
||||
expect(executor.execute).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('keeps the timeout failure path when timeout commandStatus is unavailable', async () => {
|
||||
const executor = fakeSequenceExecutor([
|
||||
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
||||
bridgeFailure('timeout', 'OpenCode commandStatus timed out', []),
|
||||
]);
|
||||
const bridge = new OpenCodeReadinessBridge(executor);
|
||||
|
||||
await expect(
|
||||
bridge.sendOpenCodeTeamMessage({
|
||||
teamId: 'team-a',
|
||||
teamName: 'team-a',
|
||||
laneId: 'secondary:opencode:bob',
|
||||
projectPath: '/repo',
|
||||
memberName: 'bob',
|
||||
text: 'hello',
|
||||
messageId: 'message-1',
|
||||
deliveryAttemptId: 'ledger-1:1:payload',
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
accepted: false,
|
||||
memberName: 'bob',
|
||||
diagnostics: [
|
||||
expect.objectContaining({
|
||||
code: 'timeout',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(executor.execute).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('keeps the timeout failure path when timeout commandStatus reports precondition mismatch', async () => {
|
||||
const executor = fakeSequenceExecutor([
|
||||
bridgeFailure('timeout', 'OpenCode bridge command timed out', []),
|
||||
bridgeCommandSuccess({
|
||||
command: 'opencode.commandStatus',
|
||||
requestId: 'status-req-1',
|
||||
data: {
|
||||
status: 'precondition_mismatch',
|
||||
safeToRetry: false,
|
||||
accepted: false,
|
||||
diagnostics: ['OpenCode command status payloadHash mismatch.'],
|
||||
},
|
||||
}),
|
||||
]);
|
||||
const bridge = new OpenCodeReadinessBridge(executor);
|
||||
|
||||
await expect(
|
||||
bridge.sendOpenCodeTeamMessage({
|
||||
teamId: 'team-a',
|
||||
teamName: 'team-a',
|
||||
laneId: 'secondary:opencode:bob',
|
||||
projectPath: '/repo',
|
||||
memberName: 'bob',
|
||||
text: 'hello',
|
||||
messageId: 'message-1',
|
||||
deliveryAttemptId: 'ledger-1:1:payload',
|
||||
})
|
||||
).resolves.toMatchObject({
|
||||
accepted: false,
|
||||
memberName: 'bob',
|
||||
diagnostics: [
|
||||
expect.objectContaining({
|
||||
code: 'timeout',
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
expect(executor.execute).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('routes state-changing launch commands through the guarded command service when configured', async () => {
|
||||
|
|
@ -610,20 +590,6 @@ function fakeSequenceExecutor(
|
|||
};
|
||||
}
|
||||
|
||||
async function withCommandStatusRecoveryEnabled<T>(callback: () => Promise<T>): Promise<T> {
|
||||
const previous = process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY;
|
||||
process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY = '1';
|
||||
try {
|
||||
return await callback();
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY;
|
||||
} else {
|
||||
process.env.CLAUDE_TEAM_OPENCODE_COMMAND_STATUS_RECOVERY = previous;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function bridgeSuccess(
|
||||
data: OpenCodeTeamLaunchReadiness
|
||||
): OpenCodeBridgeSuccess<OpenCodeTeamLaunchReadiness> {
|
||||
|
|
|
|||
|
|
@ -187,6 +187,29 @@ describe('TeamLaunchFailureArtifactPack', () => {
|
|||
).toBe('provider_quota');
|
||||
});
|
||||
|
||||
it('classifies Claude Code workspace trust failures separately', () => {
|
||||
const reason =
|
||||
'Teammate "Gayani" cannot start in headless process runtime because workspace trust is not accepted for "C:\\Users\\vilok\\OneDrive\\Desktop\\Safar 0.1". Open that workspace once interactively and accept trust, then launch the team again.';
|
||||
|
||||
const classification = classifyLaunchFailureArtifact({
|
||||
teamName: 'artifact-team',
|
||||
runId: 'run-workspace-trust',
|
||||
reason: 'Deterministic bootstrap failed',
|
||||
memberSpawnStatuses: {
|
||||
Gayani: {
|
||||
status: 'error',
|
||||
launchState: 'failed_to_start',
|
||||
hardFailureReason: reason,
|
||||
updatedAt: '2026-05-12T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
progressTraceLines: [reason],
|
||||
});
|
||||
|
||||
expect(classification.code).toBe('workspace_trust_required');
|
||||
expect(classification.evidence.join('\n')).toContain('workspace trust is not accepted');
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'stdin warning',
|
||||
|
|
|
|||
|
|
@ -207,6 +207,53 @@ describe('TeamMemberLogsFinder', () => {
|
|||
expect(projectResolver.getContext).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('dedupes concurrent forceRefresh log source discovery for the same team', async () => {
|
||||
const teamName = 'dedupe-force-refresh-context-team';
|
||||
let resolveContext!: (value: unknown) => void;
|
||||
const contextPromise = new Promise((resolve) => {
|
||||
resolveContext = resolve;
|
||||
});
|
||||
const projectResolver = {
|
||||
getContext: vi.fn(() => contextPromise),
|
||||
getLiveBaseContext: vi.fn(),
|
||||
};
|
||||
const inboxReader = { listInboxNames: vi.fn(async () => []) };
|
||||
const membersMetaStore = { getMembers: vi.fn(async () => []) };
|
||||
const finder = new TeamMemberLogsFinder(
|
||||
undefined,
|
||||
inboxReader as never,
|
||||
membersMetaStore as never,
|
||||
projectResolver as never
|
||||
);
|
||||
|
||||
const first = finder.getLogSourceWatchContext(teamName, { forceRefresh: true });
|
||||
const second = finder.getLogSourceWatchContext(teamName, { forceRefresh: true });
|
||||
await Promise.resolve();
|
||||
|
||||
expect(projectResolver.getContext).toHaveBeenCalledTimes(1);
|
||||
resolveContext({
|
||||
projectDir: '/tmp/project',
|
||||
projectId: 'project',
|
||||
sessionIds: ['session-1'],
|
||||
config: { name: teamName, projectPath: '/repo', members: [] },
|
||||
});
|
||||
|
||||
await expect(Promise.all([first, second])).resolves.toEqual([
|
||||
{
|
||||
projectDir: '/tmp/project',
|
||||
projectPath: '/repo',
|
||||
leadSessionId: undefined,
|
||||
sessionIds: ['session-1'],
|
||||
},
|
||||
{
|
||||
projectDir: '/tmp/project',
|
||||
projectPath: '/repo',
|
||||
leadSessionId: undefined,
|
||||
sessionIds: ['session-1'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns subagent logs for a member and lead session for team-lead', async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-logs-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
|
|
@ -531,6 +578,186 @@ describe('TeamMemberLogsFinder', () => {
|
|||
expect(refs.some((ref) => ref.memberName === 'Tom')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not leak old same-workspace subagent logs into a newly created team', async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-logs-scope-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
|
||||
const teamName = 'aurora-room-scope';
|
||||
const projectPath = '/Users/test/shared-workspace';
|
||||
const projectId = '-Users-test-shared-workspace';
|
||||
const leadSessionId = 'fresh-lead-session';
|
||||
const unrelatedSessionId = 'old-other-team-session';
|
||||
const now = new Date();
|
||||
|
||||
await fs.mkdir(path.join(tmpDir, 'teams', teamName), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'teams', teamName, 'config.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: teamName,
|
||||
projectPath,
|
||||
leadSessionId,
|
||||
members: [
|
||||
{ name: 'team-lead', agentType: 'team-lead' },
|
||||
{ name: 'Alice', agentType: 'general-purpose' },
|
||||
],
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const projectRoot = path.join(tmpDir, 'projects', projectId);
|
||||
const currentSubagentsDir = path.join(projectRoot, leadSessionId, 'subagents');
|
||||
const unrelatedSubagentsDir = path.join(projectRoot, unrelatedSessionId, 'subagents');
|
||||
await fs.mkdir(currentSubagentsDir, { recursive: true });
|
||||
await fs.mkdir(unrelatedSubagentsDir, { recursive: true });
|
||||
|
||||
const leadPath = path.join(projectRoot, `${leadSessionId}.jsonl`);
|
||||
await fs.writeFile(
|
||||
leadPath,
|
||||
JSON.stringify({
|
||||
timestamp: now.toISOString(),
|
||||
type: 'user',
|
||||
message: { role: 'user', content: `Lead for team "${teamName}" (${teamName})` },
|
||||
}) + '\n',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const currentAlicePath = path.join(currentSubagentsDir, 'agent-alice.jsonl');
|
||||
await fs.writeFile(
|
||||
currentAlicePath,
|
||||
JSON.stringify({
|
||||
timestamp: now.toISOString(),
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: `You are Alice, a developer on team "${teamName}" (${teamName}).`,
|
||||
},
|
||||
}) + '\n',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const unrelatedAlicePath = path.join(unrelatedSubagentsDir, 'agent-alice.jsonl');
|
||||
await fs.writeFile(
|
||||
unrelatedAlicePath,
|
||||
JSON.stringify({
|
||||
timestamp: now.toISOString(),
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: 'You are Alice, a developer on team "old-team" (old-team).',
|
||||
},
|
||||
}) + '\n',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const refs = await new TeamMemberLogsFinder().findRecentMemberLogFileRefsByMember(
|
||||
teamName,
|
||||
['team-lead', 'Alice'],
|
||||
{ forceRefresh: true }
|
||||
);
|
||||
|
||||
expect(refs).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ memberName: 'team-lead', filePath: leadPath }),
|
||||
expect.objectContaining({ memberName: 'Alice', filePath: currentAlicePath }),
|
||||
])
|
||||
);
|
||||
expect(refs.some((ref) => ref.filePath === unrelatedAlicePath)).toBe(false);
|
||||
});
|
||||
|
||||
it('can skip untracked team subagent session discovery for graph previews', async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-logs-preview-scope-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
|
||||
const teamName = 'preview-known-session-scope';
|
||||
const projectPath = '/Users/test/preview-known-session-scope';
|
||||
const projectId = '-Users-test-preview-known-session-scope';
|
||||
const leadSessionId = 'known-lead-session';
|
||||
const untrackedSessionId = 'team-subagent-only-session';
|
||||
const now = new Date();
|
||||
|
||||
await fs.mkdir(path.join(tmpDir, 'teams', teamName), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'teams', teamName, 'config.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: teamName,
|
||||
projectPath,
|
||||
leadSessionId,
|
||||
members: [
|
||||
{ name: 'team-lead', agentType: 'team-lead' },
|
||||
{ name: 'Alice', agentType: 'general-purpose' },
|
||||
{ name: 'Bob', agentType: 'general-purpose' },
|
||||
],
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const projectRoot = path.join(tmpDir, 'projects', projectId);
|
||||
const knownSubagentsDir = path.join(projectRoot, leadSessionId, 'subagents');
|
||||
const untrackedSubagentsDir = path.join(projectRoot, untrackedSessionId, 'subagents');
|
||||
await fs.mkdir(knownSubagentsDir, { recursive: true });
|
||||
await fs.mkdir(untrackedSubagentsDir, { recursive: true });
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(projectRoot, `${leadSessionId}.jsonl`),
|
||||
JSON.stringify({
|
||||
timestamp: now.toISOString(),
|
||||
type: 'user',
|
||||
message: { role: 'user', content: `Lead for team "${teamName}" (${teamName})` },
|
||||
}) + '\n',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const knownAlicePath = path.join(knownSubagentsDir, 'agent-alice.jsonl');
|
||||
await fs.writeFile(
|
||||
knownAlicePath,
|
||||
JSON.stringify({
|
||||
timestamp: now.toISOString(),
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: `You are Alice, a developer on team "${teamName}" (${teamName}).`,
|
||||
},
|
||||
}) + '\n',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const untrackedBobPath = path.join(untrackedSubagentsDir, 'agent-bob.jsonl');
|
||||
await fs.writeFile(
|
||||
untrackedBobPath,
|
||||
JSON.stringify({
|
||||
timestamp: now.toISOString(),
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: `You are Bob, a developer on team "${teamName}" (${teamName}).`,
|
||||
},
|
||||
}) + '\n',
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const finder = new TeamMemberLogsFinder();
|
||||
const fastRefs = await finder.findRecentMemberLogFileRefsByMember(teamName, ['Alice', 'Bob'], {
|
||||
forceRefresh: true,
|
||||
includeTeamSubagentSessionDiscovery: false,
|
||||
});
|
||||
expect(fastRefs.some((ref) => ref.filePath === knownAlicePath)).toBe(true);
|
||||
expect(fastRefs.some((ref) => ref.filePath === untrackedBobPath)).toBe(false);
|
||||
|
||||
const fullRefs = await finder.findRecentMemberLogFileRefsByMember(teamName, ['Alice', 'Bob'], {
|
||||
forceRefresh: true,
|
||||
});
|
||||
expect(fullRefs.some((ref) => ref.filePath === knownAlicePath)).toBe(true);
|
||||
expect(fullRefs.some((ref) => ref.filePath === untrackedBobPath)).toBe(true);
|
||||
});
|
||||
|
||||
it('applies recent-ref object options to discovery, lead refs, metadata, and requested-member attribution', async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-team-logs-'));
|
||||
setClaudeBasePathOverride(tmpDir);
|
||||
|
|
|
|||
|
|
@ -703,6 +703,87 @@ describe('TeamProvisioningService', () => {
|
|||
});
|
||||
|
||||
describe('team launch notifications', () => {
|
||||
it('fires team launched when the last pending teammate joins after ready', async () => {
|
||||
const { NotificationManager } =
|
||||
await import('@main/services/infrastructure/NotificationManager');
|
||||
const addTeamNotification = vi.fn(async (_payload: unknown) => undefined);
|
||||
NotificationManager.setInstance({ addTeamNotification } as never);
|
||||
|
||||
try {
|
||||
const svc = new TeamProvisioningService();
|
||||
const run = {
|
||||
runId: 'run-late-all-joined',
|
||||
teamName: 'late-all-joined-team',
|
||||
isLaunch: true,
|
||||
provisioningComplete: true,
|
||||
processKilled: false,
|
||||
cancelRequested: false,
|
||||
progress: { state: 'ready' },
|
||||
request: {
|
||||
cwd: tempClaudeRoot,
|
||||
displayName: 'late-all-joined-team',
|
||||
},
|
||||
expectedMembers: ['alice', 'bob'],
|
||||
allEffectiveMembers: [{ name: 'alice' }, { name: 'bob' }],
|
||||
memberSpawnStatuses: new Map([
|
||||
[
|
||||
'alice',
|
||||
createMemberSpawnStatusEntry({
|
||||
status: 'online',
|
||||
launchState: 'confirmed_alive',
|
||||
runtimeAlive: true,
|
||||
bootstrapConfirmed: true,
|
||||
}),
|
||||
],
|
||||
[
|
||||
'bob',
|
||||
createMemberSpawnStatusEntry({
|
||||
status: 'waiting',
|
||||
launchState: 'runtime_pending_bootstrap',
|
||||
runtimeAlive: true,
|
||||
bootstrapConfirmed: false,
|
||||
}),
|
||||
],
|
||||
]),
|
||||
};
|
||||
(svc as any).runs.set(run.runId, run);
|
||||
(svc as any).aliveRunByTeam.set(run.teamName, run.runId);
|
||||
|
||||
(svc as any).emitMemberSpawnChange(run, 'bob');
|
||||
expect(addTeamNotification).not.toHaveBeenCalled();
|
||||
|
||||
run.memberSpawnStatuses.set(
|
||||
'bob',
|
||||
createMemberSpawnStatusEntry({
|
||||
status: 'online',
|
||||
launchState: 'confirmed_alive',
|
||||
runtimeAlive: true,
|
||||
bootstrapConfirmed: true,
|
||||
})
|
||||
);
|
||||
|
||||
(svc as any).emitMemberSpawnChange(run, 'bob');
|
||||
await Promise.resolve();
|
||||
|
||||
expect(addTeamNotification).toHaveBeenCalledTimes(1);
|
||||
expect(addTeamNotification).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
teamEventType: 'team_launched',
|
||||
teamName: 'late-all-joined-team',
|
||||
dedupeKey: 'team_launched:late-all-joined-team:run-late-all-joined',
|
||||
body:
|
||||
'Team "late-all-joined-team" has been launched - all 2 teammates joined and are ready for tasks.',
|
||||
})
|
||||
);
|
||||
|
||||
(svc as any).emitMemberSpawnChange(run, 'bob');
|
||||
await Promise.resolve();
|
||||
expect(addTeamNotification).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
NotificationManager.resetInstance();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not fire incomplete notification for pending-only teammates still joining', async () => {
|
||||
const { NotificationManager } =
|
||||
await import('@main/services/infrastructure/NotificationManager');
|
||||
|
|
@ -17476,6 +17557,58 @@ describe('TeamProvisioningService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('reports workspace trust failures with a specific deterministic bootstrap title', () => {
|
||||
const reason =
|
||||
'Teammate "Gayani" cannot start in headless process runtime because workspace trust is not accepted for "C:\\Users\\vilok\\OneDrive\\Desktop\\Safar 0.1". Open that workspace once interactively and accept trust, then launch the team again.';
|
||||
const progressUpdates: any[] = [];
|
||||
const run = createMemberSpawnRun({
|
||||
runId: 'run-workspace-trust-bootstrap',
|
||||
teamName: 'workspace-trust-bootstrap-team',
|
||||
expectedMembers: ['Gayani'],
|
||||
});
|
||||
Object.assign(run, {
|
||||
cancelRequested: false,
|
||||
isLaunch: false,
|
||||
lastDeterministicBootstrapSeq: 0,
|
||||
progress: {
|
||||
runId: run.runId,
|
||||
teamName: run.teamName,
|
||||
state: 'assembling',
|
||||
message: 'Spawning teammate runtimes',
|
||||
startedAt: '2026-05-12T10:00:00.000Z',
|
||||
updatedAt: '2026-05-12T10:00:00.000Z',
|
||||
},
|
||||
onProgress: (progress: any) => {
|
||||
progressUpdates.push(progress);
|
||||
},
|
||||
});
|
||||
const svc = new TeamProvisioningService();
|
||||
vi.spyOn(svc as any, 'persistLaunchStateSnapshot').mockResolvedValue(null);
|
||||
vi.spyOn(svc as any, 'cleanupRun').mockImplementation(() => {});
|
||||
|
||||
const handled = (svc as any).handleDeterministicBootstrapEvent(run, {
|
||||
type: 'system',
|
||||
subtype: 'team_bootstrap',
|
||||
event: 'failed',
|
||||
reason,
|
||||
run_id: run.runId,
|
||||
team_name: run.teamName,
|
||||
seq: 1,
|
||||
});
|
||||
|
||||
expect(handled).toBe(true);
|
||||
expect(progressUpdates.at(-1)).toMatchObject({
|
||||
state: 'failed',
|
||||
message: 'Workspace trust required',
|
||||
error: reason,
|
||||
});
|
||||
expect(run.memberSpawnStatuses.get('Gayani')).toMatchObject({
|
||||
status: 'error',
|
||||
launchState: 'failed_to_start',
|
||||
hardFailureReason: reason,
|
||||
});
|
||||
});
|
||||
|
||||
it('clears stale failed_to_start state when live runtime metadata proves the teammate is alive', async () => {
|
||||
const svc = new TeamProvisioningService();
|
||||
(svc as any).getLiveTeamAgentRuntimeMetadata = vi.fn(
|
||||
|
|
|
|||
|
|
@ -136,7 +136,11 @@ describe('TeamTranscriptProjectResolver', () => {
|
|||
},
|
||||
];
|
||||
|
||||
await fs.writeFile(jsonlPath, `${lines.map((line) => JSON.stringify(line)).join('\n')}\n`, 'utf8');
|
||||
await fs.writeFile(
|
||||
jsonlPath,
|
||||
`${lines.map((line) => JSON.stringify(line)).join('\n')}\n`,
|
||||
'utf8'
|
||||
);
|
||||
return { projectDir, jsonlPath };
|
||||
}
|
||||
|
||||
|
|
@ -257,7 +261,9 @@ describe('TeamTranscriptProjectResolver', () => {
|
|||
const staleProjectPath = '/Users/test/hookplex';
|
||||
const repairedProjectPath = '/Users/test/plugin-kit-ai';
|
||||
const historicalSessionId = 'lead-old';
|
||||
await fs.mkdir(path.join(tmpDir!, 'projects', encodePath(staleProjectPath)), { recursive: true });
|
||||
await fs.mkdir(path.join(tmpDir!, 'projects', encodePath(staleProjectPath)), {
|
||||
recursive: true,
|
||||
});
|
||||
const repaired = await createSessionFile(repairedProjectPath, historicalSessionId);
|
||||
|
||||
await writeTeamConfig(teamName, {
|
||||
|
|
@ -391,7 +397,9 @@ describe('TeamTranscriptProjectResolver', () => {
|
|||
expect(warnSpy.mock.calls).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Transcript project resolution ambiguous across exact-session candidates'),
|
||||
expect.stringContaining(
|
||||
'Transcript project resolution ambiguous across exact-session candidates'
|
||||
),
|
||||
]),
|
||||
])
|
||||
);
|
||||
|
|
@ -427,7 +435,9 @@ describe('TeamTranscriptProjectResolver', () => {
|
|||
expect(warnSpy.mock.calls).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Transcript project resolution ambiguous across exact-session candidates'),
|
||||
expect.stringContaining(
|
||||
'Transcript project resolution ambiguous across exact-session candidates'
|
||||
),
|
||||
]),
|
||||
])
|
||||
);
|
||||
|
|
@ -510,4 +520,61 @@ describe('TeamTranscriptProjectResolver', () => {
|
|||
expect(context?.projectDir).toBe(repaired.projectDir);
|
||||
expect(context?.config.projectPath).toBe(repairedProjectPath);
|
||||
});
|
||||
|
||||
it('bounds root session discovery by team lifecycle in fast preview context', async () => {
|
||||
await setupClaudeRoot();
|
||||
|
||||
const teamName = 'fast-preview-team';
|
||||
const projectPath = '/Users/test/fast-preview';
|
||||
const createdAt = Date.parse('2026-04-18T12:00:00.000Z');
|
||||
const leadSessionId = 'lead-fast';
|
||||
const lead = await createTeamAwareSessionFile(projectPath, leadSessionId, teamName, 'text');
|
||||
const recent = await createTeamAwareSessionFile(
|
||||
projectPath,
|
||||
'recent-member-session',
|
||||
teamName,
|
||||
'text'
|
||||
);
|
||||
const old = await createTeamAwareSessionFile(
|
||||
projectPath,
|
||||
'old-member-session',
|
||||
teamName,
|
||||
'text'
|
||||
);
|
||||
await fs.utimes(lead.jsonlPath, new Date(createdAt + 60_000), new Date(createdAt + 60_000));
|
||||
await fs.utimes(
|
||||
recent.jsonlPath,
|
||||
new Date(createdAt + 5 * 60_000),
|
||||
new Date(createdAt + 5 * 60_000)
|
||||
);
|
||||
await fs.utimes(
|
||||
old.jsonlPath,
|
||||
new Date(createdAt - 25 * 60 * 60_000),
|
||||
new Date(createdAt - 25 * 60 * 60_000)
|
||||
);
|
||||
|
||||
await writeTeamConfig(teamName, {
|
||||
name: 'Fast Preview Team',
|
||||
createdAt,
|
||||
projectPath,
|
||||
leadSessionId,
|
||||
members: [
|
||||
{ name: 'team-lead', agentType: 'team-lead', joinedAt: createdAt, cwd: projectPath },
|
||||
{ name: 'alice', agentType: 'general-purpose', joinedAt: createdAt + 5 * 60_000 },
|
||||
],
|
||||
} as TeamConfig);
|
||||
|
||||
const resolver = new TeamTranscriptProjectResolver();
|
||||
const fastContext = await resolver.getContext(teamName, {
|
||||
forceRefresh: true,
|
||||
includeTeamSubagentSessionDiscovery: false,
|
||||
});
|
||||
const fullContext = await resolver.getContext(teamName, { forceRefresh: true });
|
||||
|
||||
expect(fastContext?.projectDir).toBe(lead.projectDir);
|
||||
expect(fastContext?.sessionIds).toEqual(expect.arrayContaining([leadSessionId]));
|
||||
expect(fastContext?.sessionIds).toContain('recent-member-session');
|
||||
expect(fastContext?.sessionIds).not.toContain('old-member-session');
|
||||
expect(fullContext?.sessionIds).toContain('old-member-session');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -107,6 +107,25 @@ describe('TeamTranscriptSourceLocator', () => {
|
|||
}) + '\n',
|
||||
'utf8'
|
||||
);
|
||||
const unrelatedSubagentPath = path.join(
|
||||
projectRoot,
|
||||
'unrelated-session-dir',
|
||||
'subagents',
|
||||
'agent-bob.jsonl'
|
||||
);
|
||||
await fs.mkdir(path.dirname(unrelatedSubagentPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
unrelatedSubagentPath,
|
||||
JSON.stringify({
|
||||
timestamp: '2026-04-15T14:02:02.000Z',
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content: 'You are bob, a developer on team "other-team" (other-team).',
|
||||
},
|
||||
}) + '\n',
|
||||
'utf8'
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(projectRoot, leadSessionId, 'subagents', 'agent-worker.jsonl'),
|
||||
JSON.stringify({
|
||||
|
|
@ -126,6 +145,7 @@ describe('TeamTranscriptSourceLocator', () => {
|
|||
expect(context?.config.projectPath).toBe(projectPath);
|
||||
expect(context?.sessionIds).toEqual(expect.arrayContaining([leadSessionId, memberSessionId]));
|
||||
expect(context?.sessionIds).not.toContain('unrelated-session');
|
||||
expect(context?.sessionIds).not.toContain('unrelated-session-dir');
|
||||
expect(transcriptFiles).toEqual(
|
||||
expect.arrayContaining([
|
||||
path.join(projectRoot, `${leadSessionId}.jsonl`),
|
||||
|
|
@ -134,6 +154,7 @@ describe('TeamTranscriptSourceLocator', () => {
|
|||
])
|
||||
);
|
||||
expect(transcriptFiles).not.toContain(path.join(projectRoot, 'unrelated-session.jsonl'));
|
||||
expect(transcriptFiles).not.toContain(unrelatedSubagentPath);
|
||||
});
|
||||
|
||||
it('returns the same sorted transcript set across multiple session directories', async () => {
|
||||
|
|
@ -193,11 +214,7 @@ describe('TeamTranscriptSourceLocator', () => {
|
|||
}) + '\n',
|
||||
'utf8'
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(subagentsDir, 'agent-acompact-ignore.jsonl'),
|
||||
'{}\n',
|
||||
'utf8'
|
||||
);
|
||||
await fs.writeFile(path.join(subagentsDir, 'agent-acompact-ignore.jsonl'), '{}\n', 'utf8');
|
||||
|
||||
expectedFiles.push(rootTranscript, subagentTranscript);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -437,9 +437,8 @@ describe('CLI status visibility during completed install state', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('shows an OpenCode download action on the dashboard when the OpenCode CLI is missing', async () => {
|
||||
it('shows an OpenCode install action on the dashboard when the OpenCode CLI is missing', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
const { api } = await import('@renderer/api');
|
||||
storeState.cliInstallerState = 'idle';
|
||||
storeState.cliStatus = createInstalledCliStatus({
|
||||
flavor: 'agent_teams_orchestrator',
|
||||
|
|
@ -478,19 +477,137 @@ describe('CLI status visibility during completed install state', () => {
|
|||
});
|
||||
|
||||
expect(host.textContent).toContain('OpenCode (75+ LLM providers)');
|
||||
expect(host.textContent).toContain('Download');
|
||||
expect(host.textContent).toContain('Install');
|
||||
|
||||
const downloadButton = Array.from(host.querySelectorAll('button')).find(
|
||||
(button) => button.textContent?.trim() === 'Download'
|
||||
const installButton = Array.from(host.querySelectorAll('button')).find(
|
||||
(button) => button.textContent?.trim() === 'Install'
|
||||
);
|
||||
expect(downloadButton).not.toBeUndefined();
|
||||
expect(installButton).not.toBeUndefined();
|
||||
|
||||
await act(async () => {
|
||||
downloadButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
installButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(api.openExternal).toHaveBeenCalledWith('https://opencode.ai/download');
|
||||
expect(storeState.installOpenCodeRuntime).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows OpenCode app-managed install progress on the dashboard provider card', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.cliInstallerState = 'idle';
|
||||
storeState.openCodeRuntimeStatus = {
|
||||
installed: false,
|
||||
source: 'missing',
|
||||
state: 'downloading',
|
||||
progress: {
|
||||
phase: 'downloading',
|
||||
downloadedBytes: 42,
|
||||
totalBytes: 100,
|
||||
percent: 42,
|
||||
detail: 'Downloading OpenCode 42%',
|
||||
},
|
||||
};
|
||||
storeState.openCodeRuntimeStatusLoading = true;
|
||||
storeState.cliStatus = createInstalledCliStatus({
|
||||
flavor: 'agent_teams_orchestrator',
|
||||
displayName: 'Multimodel runtime',
|
||||
supportsSelfUpdate: false,
|
||||
showVersionDetails: false,
|
||||
showBinaryPath: false,
|
||||
authLoggedIn: false,
|
||||
providers: [
|
||||
{
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode (75+ LLM providers)',
|
||||
supported: false,
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
verificationState: 'error',
|
||||
statusMessage: 'OpenCode CLI is not installed.',
|
||||
models: [],
|
||||
canLoginFromUi: false,
|
||||
capabilities: {
|
||||
teamLaunch: false,
|
||||
oneShot: false,
|
||||
},
|
||||
backend: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
await act(async () => {
|
||||
root.render(React.createElement(CliStatusBanner));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('Downloading 42%');
|
||||
const progressButton = Array.from(host.querySelectorAll('button')).find(
|
||||
(button) => button.textContent?.trim() === 'Downloading 42%'
|
||||
);
|
||||
expect(progressButton).not.toBeUndefined();
|
||||
expect(progressButton).toHaveProperty('disabled', true);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('always shows a provider-level Free models badge on the OpenCode dashboard card', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.cliInstallerState = 'idle';
|
||||
storeState.cliStatus = createInstalledCliStatus({
|
||||
flavor: 'agent_teams_orchestrator',
|
||||
displayName: 'Multimodel runtime',
|
||||
supportsSelfUpdate: false,
|
||||
showVersionDetails: false,
|
||||
showBinaryPath: false,
|
||||
authLoggedIn: true,
|
||||
providers: [
|
||||
{
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode (75+ LLM providers)',
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
authMethod: 'opencode_managed',
|
||||
verificationState: 'verified',
|
||||
statusMessage: 'Ready',
|
||||
models: [],
|
||||
canLoginFromUi: false,
|
||||
capabilities: {
|
||||
teamLaunch: true,
|
||||
oneShot: false,
|
||||
},
|
||||
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||
modelCatalog: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
await act(async () => {
|
||||
root.render(React.createElement(CliStatusBanner));
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const providerFreeBadge = host.querySelector('[title*="Big Pickle"]');
|
||||
expect(providerFreeBadge?.textContent).toBe('Free models');
|
||||
expect(providerFreeBadge?.getAttribute('title')).toContain('OpenRouter');
|
||||
expect(providerFreeBadge?.getAttribute('title')).toContain(
|
||||
'not every OpenCode/OpenRouter model is free'
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
|
|
|
|||
|
|
@ -348,6 +348,7 @@ describe('ExtensionStoreView provider loading placeholders', () => {
|
|||
|
||||
expect(storeState.bootstrapCliStatus).toHaveBeenCalledWith({ multimodelEnabled: true });
|
||||
expect(storeState.fetchCliStatus).not.toHaveBeenCalled();
|
||||
expect(storeState.fetchApiKeys).not.toHaveBeenCalled();
|
||||
|
||||
expect(host.textContent).toContain('Multimodel runtime capabilities');
|
||||
expect(host.textContent).toContain('Anthropic');
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { CliInstallationStatus } from '@shared/types';
|
|||
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
|
||||
|
||||
interface StoreState {
|
||||
apiKeys: Array<{
|
||||
apiKeys: {
|
||||
id: string;
|
||||
providerId: string;
|
||||
displayName: string;
|
||||
|
|
@ -15,13 +15,14 @@ interface StoreState {
|
|||
scope: 'user';
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}>;
|
||||
}[];
|
||||
apiKeysLoading: boolean;
|
||||
apiKeysError: string | null;
|
||||
apiKeyStorageStatus: {
|
||||
encryptionMethod: 'os-keychain' | 'local-aes';
|
||||
backend: string;
|
||||
} | null;
|
||||
fetchApiKeys: ReturnType<typeof vi.fn>;
|
||||
fetchApiKeyStorageStatus: ReturnType<typeof vi.fn>;
|
||||
cliStatus: CliInstallationStatus | null;
|
||||
cliStatusLoading: boolean;
|
||||
|
|
@ -187,6 +188,7 @@ describe('ApiKeysPanel', () => {
|
|||
encryptionMethod: 'os-keychain',
|
||||
backend: 'Keychain Access',
|
||||
};
|
||||
storeState.fetchApiKeys = vi.fn().mockResolvedValue(undefined);
|
||||
storeState.fetchApiKeyStorageStatus = vi.fn().mockResolvedValue(undefined);
|
||||
storeState.cliStatus = createCliStatus();
|
||||
storeState.cliStatusLoading = false;
|
||||
|
|
@ -253,6 +255,8 @@ describe('ApiKeysPanel', () => {
|
|||
expect(host.textContent).toContain('Connected');
|
||||
expect(host.textContent).toContain('Current source: Detected from OPENAI_API_KEY.');
|
||||
expect(host.textContent).toContain('ChatGPT account ready');
|
||||
expect(storeState.fetchApiKeys).toHaveBeenCalledTimes(1);
|
||||
expect(storeState.fetchApiKeyStorageStatus).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
|
|
|
|||
|
|
@ -109,7 +109,115 @@ describe('ProviderModelBadges', () => {
|
|||
expect(host.textContent).toContain('Free');
|
||||
});
|
||||
|
||||
it('collapses long model lists and expands them into a bounded scroll area', () => {
|
||||
it('renders paid and free OpenCode models together without marking every model free', () => {
|
||||
const host = render(
|
||||
<ProviderModelBadges
|
||||
providerId="opencode"
|
||||
models={['opencode/big-pickle', 'openai/gpt-5.4']}
|
||||
providerStatus={{
|
||||
providerId: 'opencode',
|
||||
authMethod: 'opencode_managed',
|
||||
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||
modelCatalog: {
|
||||
schemaVersion: 1,
|
||||
providerId: 'opencode',
|
||||
source: 'app-server',
|
||||
status: 'ready',
|
||||
fetchedAt: '2026-05-12T00:00:00.000Z',
|
||||
staleAt: '2026-05-12T00:10:00.000Z',
|
||||
defaultModelId: 'opencode/big-pickle',
|
||||
defaultLaunchModel: 'opencode/big-pickle',
|
||||
models: [
|
||||
{
|
||||
id: 'opencode/big-pickle',
|
||||
launchModel: 'opencode/big-pickle',
|
||||
displayName: 'opencode/big-pickle',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: true,
|
||||
isDefault: true,
|
||||
upgrade: false,
|
||||
source: 'app-server',
|
||||
badgeLabel: 'Free',
|
||||
},
|
||||
{
|
||||
id: 'openai/gpt-5.4',
|
||||
launchModel: 'openai/gpt-5.4',
|
||||
displayName: 'openai/gpt-5.4',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: true,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'app-server',
|
||||
badgeLabel: null,
|
||||
},
|
||||
],
|
||||
diagnostics: {
|
||||
configReadState: 'ready',
|
||||
appServerState: 'healthy',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(host.textContent).toContain('big-pickle');
|
||||
expect(host.textContent).toContain('GPT-5.4');
|
||||
expect(host.textContent?.match(/Free/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not duplicate a catalog badge that matches the displayed model label', () => {
|
||||
const host = render(
|
||||
<ProviderModelBadges
|
||||
providerId="anthropic"
|
||||
models={['claude-opus-4-6']}
|
||||
providerStatus={{
|
||||
providerId: 'anthropic',
|
||||
authMethod: 'oauth_token',
|
||||
backend: { kind: 'anthropic', label: 'Anthropic' },
|
||||
modelCatalog: {
|
||||
schemaVersion: 1,
|
||||
providerId: 'anthropic',
|
||||
source: 'anthropic-models-api',
|
||||
status: 'ready',
|
||||
fetchedAt: '2026-05-12T00:00:00.000Z',
|
||||
staleAt: '2026-05-12T00:10:00.000Z',
|
||||
defaultModelId: 'claude-opus-4-6',
|
||||
defaultLaunchModel: 'claude-opus-4-6',
|
||||
models: [
|
||||
{
|
||||
id: 'claude-opus-4-6',
|
||||
launchModel: 'claude-opus-4-6',
|
||||
displayName: 'Opus 4.6',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: true,
|
||||
isDefault: true,
|
||||
upgrade: false,
|
||||
source: 'anthropic-models-api',
|
||||
badgeLabel: 'Opus 4.6',
|
||||
},
|
||||
],
|
||||
diagnostics: {
|
||||
configReadState: 'ready',
|
||||
appServerState: 'healthy',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(host.textContent?.match(/Opus 4\.6/g)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('collapses long model lists and expands them inline without an internal scroll area', () => {
|
||||
const models = Array.from(
|
||||
{ length: 18 },
|
||||
(_, index) => `model-${String(index + 1).padStart(2, '0')}`
|
||||
|
|
@ -134,8 +242,8 @@ describe('ProviderModelBadges', () => {
|
|||
expect(host.textContent).toContain('model-18');
|
||||
expect(host.textContent).toContain('Hide');
|
||||
const list = host.firstElementChild?.firstElementChild as HTMLElement | null;
|
||||
expect(list?.style.maxHeight).toBe('200px');
|
||||
expect(list?.style.overflowY).toBe('auto');
|
||||
expect(list?.style.maxHeight).toBe('');
|
||||
expect(list?.style.overflowY).toBe('');
|
||||
|
||||
const hideButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||
button.textContent?.includes('Hide')
|
||||
|
|
@ -149,4 +257,41 @@ describe('ProviderModelBadges', () => {
|
|||
expect(host.textContent).not.toContain('model-16');
|
||||
expect(host.textContent).toContain('+3 more');
|
||||
});
|
||||
|
||||
it('limits collapsed model badges by rendered rows when requested', () => {
|
||||
const originalOffsetTop = Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'offsetTop');
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetTop', {
|
||||
configurable: true,
|
||||
get() {
|
||||
const siblings = Array.from(this.parentElement?.children ?? []);
|
||||
const index = Math.max(0, siblings.indexOf(this));
|
||||
return Math.floor(index / 3) * 20;
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const models = Array.from(
|
||||
{ length: 18 },
|
||||
(_, index) => `model-${String(index + 1).padStart(2, '0')}`
|
||||
);
|
||||
const host = render(
|
||||
<ProviderModelBadges
|
||||
providerId="codex"
|
||||
models={models}
|
||||
collapseAfter={15}
|
||||
maxCollapsedRows={2}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(host.textContent).toContain('model-05');
|
||||
expect(host.textContent).not.toContain('model-06');
|
||||
expect(host.textContent).toContain('+13 more');
|
||||
} finally {
|
||||
if (originalOffsetTop) {
|
||||
Object.defineProperty(HTMLElement.prototype, 'offsetTop', originalOffsetTop);
|
||||
} else {
|
||||
delete (HTMLElement.prototype as { offsetTop?: number }).offsetTop;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -123,6 +123,66 @@ describe('MemberList spawn-status memoization', () => {
|
|||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('does not label an empty roster as solo when the team summary still expects teammates', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(MemberList, {
|
||||
members: [],
|
||||
expectedTeammateCount: 2,
|
||||
isTeamAlive: false,
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('Team members are loading');
|
||||
expect(host.textContent).not.toContain('Solo team');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render a lead-only roster while expected teammates are still loading', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(MemberList, {
|
||||
members: [
|
||||
{
|
||||
...member,
|
||||
name: 'team-lead',
|
||||
agentType: 'team-lead',
|
||||
role: 'Team Lead',
|
||||
},
|
||||
],
|
||||
expectedTeammateCount: 2,
|
||||
isTeamAlive: false,
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('Team members are loading');
|
||||
expect(host.querySelector('[data-testid="member-team-lead"]')).toBeNull();
|
||||
expect(host.textContent).not.toContain('Solo team');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('rerenders cards when only the hard failure reason changes', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
const host = document.createElement('div');
|
||||
|
|
|
|||
|
|
@ -178,8 +178,8 @@ describe('GraphView pan interactions', () => {
|
|||
kind: 'task',
|
||||
label: 'Task 1',
|
||||
state: 'idle',
|
||||
x: 160,
|
||||
y: 90,
|
||||
x: 300,
|
||||
y: 180,
|
||||
domainRef: { kind: 'task', teamName: 'demo-team', taskId: 'task:1' },
|
||||
};
|
||||
const edge: GraphEdge = {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { GraphNode } from '@claude-teams/agent-graph';
|
|||
|
||||
function createMockContext() {
|
||||
const arcCalls: Array<{ x: number; y: number; radius: number }> = [];
|
||||
const fillTextCalls: Array<{ text: string; x: number; y: number }> = [];
|
||||
const gradient = { addColorStop: vi.fn() };
|
||||
let fillStyle: string | CanvasGradient | CanvasPattern = '';
|
||||
let globalAlpha = 1;
|
||||
|
|
@ -34,7 +35,9 @@ function createMockContext() {
|
|||
createRadialGradient: vi.fn(() => gradient),
|
||||
createLinearGradient: vi.fn(() => gradient),
|
||||
measureText: vi.fn((text: string) => ({ width: text.length * 4.5 })),
|
||||
fillText: vi.fn(),
|
||||
fillText: vi.fn((text: string, x: number, y: number) => {
|
||||
fillTextCalls.push({ text: String(text), x, y });
|
||||
}),
|
||||
strokeText: vi.fn(),
|
||||
shadowColor: '',
|
||||
shadowBlur: 0,
|
||||
|
|
@ -59,7 +62,7 @@ function createMockContext() {
|
|||
},
|
||||
} as unknown as CanvasRenderingContext2D;
|
||||
|
||||
return { ctx, arcCalls };
|
||||
return { ctx, arcCalls, fillTextCalls };
|
||||
}
|
||||
|
||||
function createTaskNode(hasLiveTaskLogs: boolean): GraphNode {
|
||||
|
|
@ -90,4 +93,37 @@ describe('drawTasks', () => {
|
|||
expect(active.arcCalls.length).toBeGreaterThanOrEqual(3);
|
||||
expect(inactive.arcCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('wraps long task subjects into two canvas lines', () => {
|
||||
const { ctx, fillTextCalls } = createMockContext();
|
||||
const node = {
|
||||
...createTaskNode(false),
|
||||
displayId: '0f505654',
|
||||
sublabel:
|
||||
'Review VitePress docs: Get missing developer page ready for publishing search analytics routing navigation validation checklist and docs',
|
||||
};
|
||||
|
||||
drawTasks(ctx, [node], 1, null, null, null, 1);
|
||||
|
||||
const firstTitleLine = fillTextCalls.find((call) => call.y === -16);
|
||||
const secondTitleLine = fillTextCalls.find((call) => call.y === 2);
|
||||
const displayId = fillTextCalls.find((call) => call.text === '0f505654');
|
||||
|
||||
expect(firstTitleLine?.text).toContain('Review VitePress docs');
|
||||
expect(secondTitleLine?.text).toContain('...');
|
||||
expect(displayId?.y).toBe(23);
|
||||
});
|
||||
|
||||
it('keeps the compact single-line subject layout for short titles', () => {
|
||||
const { ctx, fillTextCalls } = createMockContext();
|
||||
drawTasks(ctx, [createTaskNode(false)], 1, null, null, null, 1);
|
||||
|
||||
const titleLine = fillTextCalls.find((call) => call.text === 'Live log task');
|
||||
const secondTitleLine = fillTextCalls.find((call) => call.y === 2);
|
||||
const displayId = fillTextCalls.find((call) => call.text === '#1');
|
||||
|
||||
expect(titleLine?.y).toBe(-12);
|
||||
expect(secondTitleLine).toBeUndefined();
|
||||
expect(displayId?.y).toBe(12);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -344,6 +344,70 @@ describe('cliInstallerSlice', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('OpenCode runtime installer actions', () => {
|
||||
it('refreshes OpenCode provider status after a successful app-managed install', async () => {
|
||||
const placeholder = createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
supported: false,
|
||||
authenticated: false,
|
||||
authMethod: null,
|
||||
verificationState: 'error',
|
||||
statusMessage: 'OpenCode CLI is not installed.',
|
||||
models: [],
|
||||
canLoginFromUi: false,
|
||||
capabilities: {
|
||||
teamLaunch: false,
|
||||
oneShot: false,
|
||||
extensions: createDefaultCliExtensionCapabilities(),
|
||||
},
|
||||
backend: null,
|
||||
});
|
||||
const refreshed = createMultimodelProvider({
|
||||
providerId: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
authMethod: 'opencode_managed',
|
||||
models: ['opencode/big-pickle'],
|
||||
canLoginFromUi: false,
|
||||
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||
});
|
||||
|
||||
useStore.setState({
|
||||
cliStatus: createMultimodelStatus([placeholder]),
|
||||
});
|
||||
vi.mocked(api.openCodeRuntime.install).mockResolvedValue({
|
||||
installed: true,
|
||||
binaryPath: '/Users/tester/App Support/runtimes/opencode/current/opencode',
|
||||
version: '1.14.48',
|
||||
source: 'app-managed',
|
||||
state: 'ready',
|
||||
});
|
||||
vi.mocked(api.cliInstaller.getProviderStatus).mockResolvedValue(refreshed);
|
||||
|
||||
await useStore.getState().installOpenCodeRuntime();
|
||||
|
||||
expect(api.openCodeRuntime.invalidateStatus).toHaveBeenCalledTimes(1);
|
||||
expect(api.cliInstaller.invalidateStatus).toHaveBeenCalledTimes(1);
|
||||
expect(api.cliInstaller.getProviderStatus).toHaveBeenCalledWith('opencode');
|
||||
expect(useStore.getState().openCodeRuntimeStatus).toMatchObject({
|
||||
installed: true,
|
||||
source: 'app-managed',
|
||||
state: 'ready',
|
||||
});
|
||||
expect(
|
||||
useStore
|
||||
.getState()
|
||||
.cliStatus?.providers.find((provider) => provider.providerId === 'opencode')
|
||||
).toMatchObject({
|
||||
supported: true,
|
||||
authenticated: true,
|
||||
models: ['opencode/big-pickle'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchCliStatus', () => {
|
||||
it('updates cliStatus from API', async () => {
|
||||
const mockStatus: CliInstallationStatus = {
|
||||
|
|
|
|||
|
|
@ -1796,6 +1796,90 @@ describe('teamSlice actions', () => {
|
|||
expect(selectResolvedMembersForTeamName(store.getState(), 'my-team')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('preserves a cached roster when full launch failure metadata confirms the member names', async () => {
|
||||
const store = createSliceStore();
|
||||
const cachedSnapshot = createTeamSnapshot({
|
||||
config: { name: 'Cached Team' },
|
||||
members: [
|
||||
{ name: 'alice', role: 'developer', currentTaskId: null },
|
||||
{ name: 'bob', role: 'reviewer', currentTaskId: null },
|
||||
],
|
||||
});
|
||||
const leadOnlySnapshot = createTeamSnapshot({
|
||||
config: { name: 'Lead Only Team' },
|
||||
members: [{ name: 'team-lead', agentType: 'team-lead', currentTaskId: null }],
|
||||
});
|
||||
|
||||
store.setState({
|
||||
teamDataCacheByName: {
|
||||
'my-team': cachedSnapshot,
|
||||
},
|
||||
teamByName: {
|
||||
'my-team': {
|
||||
teamName: 'my-team',
|
||||
displayName: 'My Team',
|
||||
description: '',
|
||||
memberCount: 0,
|
||||
expectedMemberCount: 2,
|
||||
partialLaunchFailure: true,
|
||||
missingMembers: ['alice', 'bob'],
|
||||
taskCount: 0,
|
||||
lastActivity: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
hoisted.getData.mockResolvedValueOnce(leadOnlySnapshot);
|
||||
|
||||
await store.getState().selectTeam('my-team');
|
||||
|
||||
expect(store.getState().selectedTeamData).toEqual(cachedSnapshot);
|
||||
expect(
|
||||
selectResolvedMembersForTeamName(store.getState(), 'my-team').map((member) => member.name)
|
||||
).toEqual(['alice', 'bob']);
|
||||
});
|
||||
|
||||
it('does not preserve a cached roster when launch failure metadata only names part of the team', async () => {
|
||||
const store = createSliceStore();
|
||||
const cachedSnapshot = createTeamSnapshot({
|
||||
config: { name: 'Cached Team' },
|
||||
members: [
|
||||
{ name: 'alice', role: 'developer', currentTaskId: null },
|
||||
{ name: 'bob', role: 'reviewer', currentTaskId: null },
|
||||
],
|
||||
});
|
||||
const leadOnlySnapshot = createTeamSnapshot({
|
||||
config: { name: 'Lead Only Team' },
|
||||
members: [{ name: 'team-lead', agentType: 'team-lead', currentTaskId: null }],
|
||||
});
|
||||
|
||||
store.setState({
|
||||
teamDataCacheByName: {
|
||||
'my-team': cachedSnapshot,
|
||||
},
|
||||
teamByName: {
|
||||
'my-team': {
|
||||
teamName: 'my-team',
|
||||
displayName: 'My Team',
|
||||
description: '',
|
||||
memberCount: 0,
|
||||
expectedMemberCount: 2,
|
||||
partialLaunchFailure: true,
|
||||
missingMembers: ['bob'],
|
||||
taskCount: 0,
|
||||
lastActivity: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
hoisted.getData.mockResolvedValueOnce(leadOnlySnapshot);
|
||||
|
||||
await store.getState().selectTeam('my-team');
|
||||
|
||||
expect(store.getState().selectedTeamData).toEqual(leadOnlySnapshot);
|
||||
expect(
|
||||
selectResolvedMembersForTeamName(store.getState(), 'my-team').map((member) => member.name)
|
||||
).toEqual(['team-lead']);
|
||||
});
|
||||
|
||||
it('commits a late selectTeam snapshot that explicitly marks members as removed', async () => {
|
||||
const store = createSliceStore();
|
||||
const selectRequest = createDeferredPromise<ReturnType<typeof createTeamSnapshot>>();
|
||||
|
|
@ -3077,6 +3161,61 @@ describe('teamSlice actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not synthesize member cards from launch failure names when summary roster is missing', () => {
|
||||
const store = createSliceStore();
|
||||
const leadOnlySnapshot = createTeamSnapshot({
|
||||
config: {
|
||||
name: 'My Team',
|
||||
projectPath: '/repo',
|
||||
},
|
||||
members: [
|
||||
{
|
||||
name: 'team-lead',
|
||||
agentType: 'team-lead',
|
||||
currentTaskId: null,
|
||||
role: 'Lead from detail',
|
||||
color: 'purple',
|
||||
},
|
||||
],
|
||||
tasks: [
|
||||
{
|
||||
id: 'task-1',
|
||||
subject: 'Build',
|
||||
status: 'in_progress',
|
||||
owner: 'Alice',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
store.setState({
|
||||
selectedTeamName: 'my-team',
|
||||
selectedTeamData: leadOnlySnapshot,
|
||||
teamDataCacheByName: {
|
||||
'my-team': leadOnlySnapshot,
|
||||
},
|
||||
teamByName: {
|
||||
'my-team': {
|
||||
teamName: 'my-team',
|
||||
displayName: 'My Team',
|
||||
description: '',
|
||||
memberCount: 0,
|
||||
expectedMemberCount: 2,
|
||||
leadName: 'Lead',
|
||||
partialLaunchFailure: true,
|
||||
missingMembers: ['Lead', 'Alice', 'bob'],
|
||||
taskCount: 1,
|
||||
lastActivity: null,
|
||||
},
|
||||
},
|
||||
memberActivityMetaByTeam: {},
|
||||
});
|
||||
|
||||
const members = selectResolvedMembersForTeamName(store.getState(), 'my-team');
|
||||
|
||||
expect(members.map((m) => m.name)).toEqual(['team-lead']);
|
||||
expect(selectResolvedMemberForTeamName(store.getState(), 'my-team', 'Alice')).toBeNull();
|
||||
});
|
||||
|
||||
it('memoizes team-scoped member messages selectors over the merged message feed', () => {
|
||||
const store = createSliceStore();
|
||||
store.setState({
|
||||
|
|
|
|||
|
|
@ -162,24 +162,24 @@ describe('teamModelAvailability', () => {
|
|||
]);
|
||||
|
||||
expect(getAvailableTeamProviderModels('opencode', providerStatus)).toEqual([
|
||||
'openai/gpt-5.4',
|
||||
'opencode/big-pickle',
|
||||
'openai/gpt-5.4',
|
||||
'openrouter/moonshotai/kimi-k2',
|
||||
]);
|
||||
|
||||
expect(getAvailableTeamProviderModelOptions('opencode', providerStatus)).toEqual([
|
||||
{ value: '', label: 'Default', badgeLabel: 'Default' },
|
||||
{
|
||||
value: 'openai/gpt-5.4',
|
||||
label: 'GPT-5.4',
|
||||
badgeLabel: 'OpenAI',
|
||||
value: 'opencode/big-pickle',
|
||||
label: 'big-pickle',
|
||||
badgeLabel: 'OpenCode',
|
||||
availabilityStatus: 'available',
|
||||
availabilityReason: null,
|
||||
},
|
||||
{
|
||||
value: 'opencode/big-pickle',
|
||||
label: 'big-pickle',
|
||||
badgeLabel: 'OpenCode',
|
||||
value: 'openai/gpt-5.4',
|
||||
label: 'GPT-5.4',
|
||||
badgeLabel: 'OpenAI',
|
||||
availabilityStatus: 'available',
|
||||
availabilityReason: null,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -43,6 +43,102 @@ describe('teamModelCatalog', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('orders OpenCode free models before paid models', () => {
|
||||
expect(
|
||||
getVisibleTeamProviderModels(
|
||||
'opencode',
|
||||
[
|
||||
'openrouter/deepseek/deepseek-r1',
|
||||
'openai/gpt-5.4',
|
||||
'openrouter/openai/gpt-oss-20b:free',
|
||||
'opencode/big-pickle',
|
||||
],
|
||||
{
|
||||
providerId: 'opencode',
|
||||
authMethod: 'opencode_managed',
|
||||
backend: { kind: 'opencode-cli', label: 'OpenCode CLI' },
|
||||
modelCatalog: {
|
||||
schemaVersion: 1,
|
||||
providerId: 'opencode',
|
||||
source: 'app-server',
|
||||
status: 'ready',
|
||||
fetchedAt: '2026-05-12T00:00:00.000Z',
|
||||
staleAt: '2026-05-12T00:10:00.000Z',
|
||||
defaultModelId: 'opencode/big-pickle',
|
||||
defaultLaunchModel: 'opencode/big-pickle',
|
||||
models: [
|
||||
{
|
||||
id: 'opencode/big-pickle',
|
||||
launchModel: 'opencode/big-pickle',
|
||||
displayName: 'opencode/big-pickle',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: true,
|
||||
isDefault: true,
|
||||
upgrade: false,
|
||||
source: 'app-server',
|
||||
badgeLabel: 'Free',
|
||||
},
|
||||
{
|
||||
id: 'openrouter/openai/gpt-oss-20b:free',
|
||||
launchModel: 'openrouter/openai/gpt-oss-20b:free',
|
||||
displayName: 'openrouter/openai/gpt-oss-20b:free',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: true,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'app-server',
|
||||
badgeLabel: 'Free',
|
||||
},
|
||||
{
|
||||
id: 'openai/gpt-5.4',
|
||||
launchModel: 'openai/gpt-5.4',
|
||||
displayName: 'openai/gpt-5.4',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: true,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'app-server',
|
||||
badgeLabel: null,
|
||||
},
|
||||
{
|
||||
id: 'openrouter/deepseek/deepseek-r1',
|
||||
launchModel: 'openrouter/deepseek/deepseek-r1',
|
||||
displayName: 'openrouter/deepseek/deepseek-r1',
|
||||
hidden: false,
|
||||
supportedReasoningEfforts: [],
|
||||
defaultReasoningEffort: null,
|
||||
inputModalities: ['text'],
|
||||
supportsPersonality: true,
|
||||
isDefault: false,
|
||||
upgrade: false,
|
||||
source: 'app-server',
|
||||
badgeLabel: null,
|
||||
},
|
||||
],
|
||||
diagnostics: {
|
||||
configReadState: 'ready',
|
||||
appServerState: 'healthy',
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
).toEqual([
|
||||
'opencode/big-pickle',
|
||||
'openrouter/openai/gpt-oss-20b:free',
|
||||
'openai/gpt-5.4',
|
||||
'openrouter/deepseek/deepseek-r1',
|
||||
]);
|
||||
});
|
||||
|
||||
it('detects Sonnet aliases with or without 1M suffix', () => {
|
||||
expect(isAnthropicSonnetTeamModel('sonnet')).toBe(true);
|
||||
expect(isAnthropicSonnetTeamModel('sonnet[1m]')).toBe(true);
|
||||
|
|
|
|||
Loading…
Reference in a new issue